学过一天前端的小白都知道,html 里面有一个标签叫做 ol——order list。通过 ol 和 li 的嵌套,我们能够得到前面有数字标号的列表。这位小白如果再多学几天,也许还会知道,css 里有一个属性叫做 list-style-type。通过它,能够控制列表标号的类型,从大小圆点、到数字、大小写英文、罗马字母不等。
不过,这位小白也许再学很久都不会接触到 css counter。这是一个古老但实用的属性,给予我们更灵活更强大地控制列表标号的能力。下面我们就来介绍它。
css counters 的属性
- counter-reset
- counter-increment
- counter() / counters()
依次说明:
1.counter-reset。
明译为计数器重置。形如:counter-reset: level1
其中,level1 只是示例,实际上是可以任意命名的一个名字标识符。
按我的理解,counter-reset 的真实意思是:在目标元素所在的层级中定义一个计数器。
2.counter-increment
明译为计数器累加。形如: counter-increment: level1 1
其中,level1 是通过 counter-reset 定义的计数器名,这里的 1 也可以是任意其他整数,甚至可以是负数。当浏览器渲染页面时,带这个属性的元素每出现一次,当前层级内对应名字的计数器就增加相应的值。如不写,默认是 1。
3.counter() / counters()
前面两个属性定义了计数器和计算规则,不过,这些计数器的计算目前都是在内存中进行的。counter() 和 counters() 就负责把计数器显示出来。这两个计算方法要和伪元素的 content 属性搭配食用。形如:content: counter(level1)
。counter 计算符前后可以随意加字符串来对最后的效果做拼接。
*注意:当 couner() 一个没有定义过的计数器时,会显示 1。
下面来看一个最简单的例子。
例1:
html:
<div class="level1">
<div class="level1-item">foo</div>
<div class="level1-item">bar</div>
</div>复制代码
css:
.level1 {
counter-reset: level1;
}
.level1-item {
counter-increment: level1 1; /* counter-increment写到伪类的选择器中也是可以的 */
}
.level1-item:before {
content: 'step' counter(level1) '.';
}复制代码
效果:
很简单是吧。至此,想必读者对 css counter 的属性的基本用法已经有所了解了。下面开始进阶了,请系好安全带。
进阶应用
使用 list-style-type
counter() 和 counters() 方法内还可以应用 list-style-type。只要把 list-style-type 属性的合法值写在计数器名后即可,中间用逗号分隔。
例2 (其余 css 和 html 保持一致):
.level1-item:before {
content: counter(level1, upper-roman) '.';
}复制代码
效果:
重新开始计数
在同一层级中,可以通过重复声明相同名字的计数器来打断(或者说是覆盖)之前的计数,重新开始。
例3:
<div class="level1">
<div class="level1-item">foo</div>
<div class="level1-item">bar</div>
<div class="level1-item break">baz</div>
<div class="level1-item">qux</div>
</div>复制代码
.level1 {
counter-reset: level1;
}
.break {
counter-reset: level1;
}
.level1-item:before {
content: counter(level1, upper-roman) '.';
counter-increment: level1 1;
}复制代码
效果:
多层嵌套计数
重点来了。到上面为止,所有的效果我们都可以用普通的 ol 标签来实现,但下面的多层嵌套计数则是 css counter 独有的绝技,光用 ol 是实现不了的。(你要手动硬写标号那当我什么都没说)而这也是 css counter 最主要的用武之地。
例4:
<div class="level1">
<div class="level1-item">
china
<div class="level1">
<div class="level1-item">newbee</div>
<div class="level1-item">
lgd
<div class="level1">
<div class="level1-item">lgd</div>
<div class="level1-item">lfy</div>
</div>
</div>
</div>
</div>
<div class="level1-item">
world
<div class="level1">
<div class="level1-item">liquid</div>
</div>
</div>
</div>复制代码
.level1 {
counter-reset: level1;
}
.level1 div {
padding-left: 10px;
}
.break {
counter-reset: level1;
}
.level1-item:before {
counter-increment: level1;
content: counters(level1, '-') '. '
}复制代码
效果:
在上面的例子里,我们首次使用了 counters() 函数,它的第一个参数是计数器名字,第二个参数是一个连接符。counters() 函数会在文档中遍历查找指定计数器,并按照嵌套关系,用连接符把不同层级上的同名计数器的值连接起来,形成标号。
成是成了,不过我认为上面这个方法并不是一个好的实践,原因有二:
第一,列表中明明出现了三个层级,但我们编码时却一直在使用 level1 这一个计数器,让人困惑。
第二,我们引入了无意义的 html 标签 <div class="level1"></div>
。比如,newbee,lgd 都是中国战队,我们希望直接放到 china 的后面,而不是用一个额外的不具备什么语义的 level1 标签来把他们包裹起来。读者也可以发现,目前的 html 结构还是比较复杂的。
使用多个计数器的多层嵌套计数
仔细思考一下,我们发现,上面的问题都是因为我们只使用了一个计数器。考虑到 content 属性里可以使用多个 counter() 函数,我们完全可以把嵌套层级拆成多个计数器。
改造上面的例子。
例5:
<div class="level1">
<div class="level1-item">
china
<div class="level2-item">newbee</div>
<div class="level2-item">
lgd
<div class="level3-item">lgd</div>
<div class="level3-item">lfy</div>
</div>
</div>
<div class="level1-item">
world
<div class="level2-item">liquid</div>
</div>
</div>复制代码
.level1 {
counter-reset: level1;
}
.level1-item:before {
counter-reset: level2;
counter-increment: level1;
content: counter(level1) '.';
}
.level2-item:before {
counter-reset: level3;
counter-increment: level2;
content: counter(level1) '-' counter(level2) '.';
}
.level3-item:before {
counter-increment: level3;
content: counter(level1) '-' counter(level2) '-' counter(level3)'.';
}复制代码
效果:
完美,和之前的一模一样。使用这种实现方式,html 的层级减少了,而通过引入了 level2 和 level3,整体的结构变得更加清晰了。
可是有的同学又要说了,不行,我还是更喜欢之前那种单个计数器的方式,一个 counters() 全搞定,酷炫。而且虽然 html 比现在多,但是 css 少呀!
嗯,说的有点道理,不过当遇到下面这种需求时,恐怕你只有使用多个计数器了。
结合嵌套计数器与自定义 list-style-tyle
上面的嵌套列表有一个特点,即每个层级的标号使用的都是数字。但实际需求可能更加灵活,比如第一级用一个大写字母,第二级用第一级标号+数字,第三级用小写罗马数字等。这种情况下,用一个 counters() 是实现不了的。
例6:
.level1 {
counter-reset: level1;
}
.level1-item:before {
counter-reset: level2;
counter-increment: level1;
content: counter(level1, upper-alpha) '.';
}
.level2-item:before {
counter-reset: level3;
counter-increment: level2;
content: counter(level1, upper-alpha) '-' counter(level2) '.';
}
.level3-item:before {
counter-increment: level3;
content: counter(level3, lower-roman)'.';
}复制代码
效果:
完美实现!
从 counters() 的标号错乱来深入理解 css counters
回过头看例4,如果我们把 html 的结构改成这样:
<div class="level1">
<div class="level1-item">国内战队</div>
<div class="level1">
<div class="level1-item">newbee</div>
<div class="level1-item">lgd</div>
<div class="level1">
<div class="level1-item">lgd</div>
<div class="level1-item">lfy</div>
</div>
<div class="level1-item">vg</div>
</div>
<div class="level1-item">国外战队</div>
<div class="level1">
<div class="level1-item">liquid</div>
</div>
</div>
</div>复制代码
css 保持不变,结果就变成了:
好吧,我们看到,原本标号应该是 1-3 的项变成了1-2-3,原本应该是 2 的项变成了 1-3,这是怎么回事儿呢?
分析当前结构和正确结构的区别,我们发现,最大的区别在于当前结构把概念上属于子级的 level1 容器写成了列表上一级的兄弟元素。还别说,如果不注意,我也很容易就写成这样了。
但为啥这么写就不对了呢?
按我的理解,计数器拥有一个活动范围,这个范围并不是计数器所在的元素本身,而是这个计数器所在的元素的父元素。换句话说,如果在一个元素上定义了计数器,那么这个元素的所有兄弟元素都能访问到这个计数器,而且将优先访问这个计数器。
优先级:
在元素自身上定义的计数器 > 在元素兄弟元素上定义的计数器 > 在元素父元素上定义的计数器
另外,元素无法访问到其兄弟元素的子元素定义的计数器。
在上面的例子中,紧跟在 level-1 后的兄弟元素 level-1-item 也能访问到 level-1 定义的计数器,而这个计数器在逻辑上已经是深层级的了。所以标号原本应该是 1-3 的项实际上读取的是它前面那个兄弟元素 level-1 定义的计数器,这个计数器实际上是第三层级,于是在原来的基础上顺着 +1,成了 1-2-3。
到头来,为什么一定要用 css counters ?
有些拼命拆我台的同学又要说了,css counters 太麻烦了,我还是不想用它。就算出现了嵌套列表的标号的需求,我直接用纯文本,把类似 A-1 这样的文字写进标签中不就行了?
行,如果你真的这么干了,我敬你是条汉子。但你有没有想过,如果在列表中增加了一条或者删除了一条,当前层级之后的所有列表项的标号,可都要变了。最极端的情况:如果产品说要把列表项的第一项删掉···?
嗯,教你两个选择。第一,打死产品。第二,打一开始就使用 css counters。
最后,来看一下兼容性
css counters 系列属性 css2 就加了,所以兼容性特别好,一片原谅色,大家放心大胆使用吧。