图解CSS:奇妙的CSS计数器世界(Part2)
在 奇妙的 CSS 计数器世界 第一部分 主要介绍了使用 list-style(以及它们的子属性list-style-type、list-style-image和list-style-position)属性给列表项设置列表项标记符样式。除此之外,还可以使用 @counter-style 规则来自定义列表项标记符(list-style-type的值)。事实上,每个列表顶的标记符都是::marker伪元素生成的标记框的内容。在这部分中,我们开始进入::marker伪元素世界。感兴趣的请继续往下阅读。
::marker 伪元素
在 CSS 中,li 元素和使用display为list-item的元素都会有一个标记框(Marker Box),对应的就是::marker伪元素。列表项或display为list-item的元素其标记符的特征(即标记符样式)是由这个标记框的样式决定的,它是一个符号或序列号。默认情况之下,无序列表的标记符是一个符号,而有序列表的标记符是一个序列号。
注意,
display除了list-item会生成列表项标记框之外,还可以是inline list-item和block list-item,有关于display更详细的介绍,可以阅读《display属性》一文。
在 CSS 布局模型中,列表项的标记由一个与每个列表项相关的标记框表示:

这个标记的内容可以通过列表项上的list-style-type和list-style-image属性以及::marker伪元素分配属性来控制。虽然,使用 list-style-image 可以引用图片来调转标记符样式或使用list-style-type使用字符以及结合@counter-style自定义的标记符给列表标记符设置样式。但他们有一定的局限性,比如要控制他们的位置,个性化的样式等。但我们可以使用::marker伪元素content定义列表项标记符,并且可以很好的对其样式做个性化方面的处理。比如说,只改变列表项标记符的 颜色、大小、位置 等,甚至还可以构建个性化非常强的标记符效果。
CSS Lists and Counters Module Level 3规范第一部分内容就是专门介绍::marker伪元素的!简单地说,列表项的标记框(Marker Box)是由一个列表项的::marker伪元素生成的,而且作为该列表项的第一个子元素。
列表项标记框的内容
试着回忆一下,默认的列表项和display为list-items的元素,::marker对应的内容是什么?这里指的是其计算之后的内容。比e如,在列表项上未显式使用 CSS 的 list-style-type 或 list-style-image 指定任何标记符样式,那么标记框(即::marker)的content的值为normal。
规范中对于标记框的内容是什么,是有明确的规则声明的。简单地说,标记框的内容是由下面这些条件中第一个为真的条件决定的:
- ①:
::marker伪元素的content的值不是normal:标记框的内容由content属性决定,和伪元素::before和::after完全一样,但当content的值为none时,::marker被移除,标记框中会没有任何内容 - ②:元素的
list-style-image显式定义了一个标记符为图像 :标记框包含了一个匿名的内联替换元素(代表指定的标记图像),并且在标记图像后面紧跟了一个空格符(U+0020,即空格键符,相当于按了键盘上的 SPACE键) - ③:元素的
list-style-type显式定义了一个标记符 :标记框包含一个由指定标记字符串组成的文本 - 否则,标记框会没有内容,并且
::marker不会生成一个框,类似于display: none一样
此外,UA可能转化为空格或丢弃任何保留的强制换行。
我们通过示例来解释。
先来看第一种情况。如果规则②和规则③都成立(为真),即没有显式给列表项设置list-style-type和list-style-image属性值,这个时候:
- 无序列表的列表项(
li)的list-style-type是disc,list-style-image为none - 有序列表的列表项(
li)的list-style-type是decimal,list-style-image为none display为list-item的元素,和无序列表项的表现相似,即list-style-type为disc,list-style-image为none
在此基础上,我们将调整 规则①。即,在::marker伪元素的content指定不同的值:
:root {
--content: normal;
}
.list-item::marker {
content: var(--content);
}
content的值运用了上一部分中提到的content可用值,最终效果如下:
当::marker的content的值为normal时,标记框的内容则是列表项list-style-type的默认值:

当::marker的content的值设置为none时,这个时候标记框会被移除,相当于display: none:
当::marker的content的值设置为inherit时,它的表现将和content取值为normal相同。
当::marker的content的值设置为空字符串(" "),列表项标记框的内容为空字符串,并且将覆盖list-style-type的默认样式。虽然标记框内容为空,但::marker不会被移除:

当::marker的content的值设置为其他格式,比如文本字符串,HTML实体符,Emoji表情符号,图像等,标记框的内容将是content的值指定的内容,并且会覆盖list-style-type的默认样式:
接下来看第二种情况,将规则②中的list-style-image显式设置一个指定的值,但规则③不变(list-style-type不显式设置任何值)。在此条件之下,同样调整规则①。即,在::marker伪元素的content指定不同的值:
:root {
--content: normal;
}
.list-item {
list-style-image: url('./rocket.svg');
}
.list-item::marker {
content: var(--content);
}
当::marker的content值为normal时,标记框的内容是list-style-image指定的图片:

当list-style-image指定的图片资源失效的时候,标记框的内容会回到list-style-type初始效果:

这也验证了前面介绍list-style-image所说的:
list-style-image生效需要确保列表项的::marker的content为normal!
当::marker的content值为none时,标记框会被移除,同时list-style-image和list-style-type都将失效,同时::marker也类似于display: none。标记框没了,所以标记符样式也将丢失:
当::marker的content值为inherit时,表现效果和normal等同,标记框的内容是list-style-image指定的内容。
当::marker的content值是其他值类型,比如文本字符,表情符号,图像等时,标记框的内容将会是content中指定的内容,同时也将覆盖list-style-image指定的图像内容:
再来看第三种情况,将规则③中的list-style-type显式设置一个指定的值,但规则②不变(list-style-image不显式设置任何值)。在此条件之下,同样调整规则①。即,在::marker伪元素的content指定不同的值:
:root {
--content: normal;
}
.list-item {
list-style-type: '±';
}
.list-item::marker {
content: var(--content);
}
当::marker的content值为normal时,标记框的内容是由list-style-type属性指定的值来生成:

当::marker的content值为none时,::marker会从DOM中移除,类似于display: none。标记框没有任何内容:
当::marker的content值为inherit时,它的表现形式和content取值为normal相同。
同样的,当::marker的content值是其他值类型,比如文本字符,表情符号,图像等时,标记框的内容将会是content中指定的内容,同时也将覆盖list-style-type指定的内容:
最后一种情况,规则②和规则③的值都改变,即list-style-type 和 list-style-image 属性显式设置值。在此条件之下,同样调整规则①。即,在::marker伪元素的content指定不同的值:
:root {
--content: normal;
}
.list-item {
list-style-type: "±";
list-style-image: url(./rocket.svg);
}
.list-item::marker {
content: var(--content);
}
当 ::marker 的 content 值为 normal 时,标记框的内容是由 list-style-image 引入的图像资源来决定,但当 list-style-image 引入的图片资源失效时,标记框的内容是 list-style-type 属性的值。
这也验证了:
list-style-type生效时,::marker的content值为normal,且list-style-image为none!
当::marker的content值为none时,::marker会从DOM中移除,类似于display: none。标记框没有任何内容。
当::marker的content值为inherit时,它的表现效果和content取值为normal相同。
当::marker的content值是其他值类型,比如文本字符,表情符号,图像等时,标记框的内容将会是content中指定的内容,同时也将覆盖list-style-type 和 list-style-image 指定的内容。
简单地总结一下:
- 当
::marker的content值为none时,::marker伪元素会从DOM中移除,相当于display: none,此时标记框不会有任何内容,即使是list-style-type和list-style-image设置了值也是如此 - 当
::marker的content值为normal时,且list-style-image和list-style-type未显式设置值时,此时标记框的内容将会是list-style-type的默认值 - 当
::marker的content值为normal时,且list-style-image显式设置了值,但list-style-type并未显式设置值,此时标记框的内容将会是list-style-image的值,要是list-style-image的图像资源失效时,将会取list-style-type的默认值作为标记框的内容 - 当
::marker的content值为normal时,且list-style-type显示设置了值,但list-style-image并未显式设置值,此时标记框的内容将会是list-style-type的值 - 当
::marker的content值为normal时,且list-style-image和list-style-type显式设置了值,此时标记框的内容将会是list-style-image的值,但当list-style-image引入的资源失效时,标记框的内容会是list-style-type的值 - 当
::marker的content值为inherit时,标记框的内容和content的normal相同 - 当
::marker的content值为非normal、none和inherit的其他数据类型,比如文本字符、表情符号、引号、图形等,即使是list-style-type和list-style-image显式设置值也会被覆盖,此时标记框的内容会是content的值,即使是空字符串(" ")也将生效,只是此时标记框的内容是空字符串,标记符无任何样式形式
特别声明,在::marker的content生成标记框内容,还可以使用counter()自动生成:
ul ::marker,
ol ::marker {
content: counter(list-item) "» ";
}
.list {
counter-reset: order;
}
.list > .list-item {
counter-increment: order;
display: list-item;
}
.list ::marker {
content: counter(order) "» ";
}
有关于这部分更详细的介绍,将放到counter()部分来介绍。
适用于 ::marker 的属性
::marker伪元素允许你针对列表标记与列表项的内容分开。在还没有::marker伪元素的时候,这是不太可能的。因此,如果你在ul( ol)或 li上改变颜色(color)或字体大小(font-size),也会改变标记符(也就是标记框中的内容)的颜色和大小。但很多时候,只是希望调整标记框内容的颜色或大小,可这样的看上去非常简单的一件事情,我们不得不去调整 HTML的结构。比如说,把列表项的内容放在一个<span>内:
<!-- HTML -->
<ul>
<li><span>List Item</span></li>
</ul>
/* CSS */
ul {
color: #00b7a8;
}
ul span {
color #333;
}
有了::marker伪元素,我们可以在不调整 HTML 结构之下就可以单独为列表项标记符设置不同的颜色。这意味着,上面的示例,我们可以像下面这样来调整:
<ul>
<li>List Item</li>
</ul>
/* CSS */
ul {
color: #333;
}
ul li::marker {
color: #00b7a8;
}
我们来看一个具体的示例:
虽然::marker伪元素可以使用CSS属性设置一些样式,弥补了list-style-type 有限的样式设计可能性,但并不意味着你能在 ::marker 上使用每一个 CSS 属性。换句话说,到目前为止,我们只能在::marker上使用下面这几个 CSS 属性:
animation-*transition-*colordirectionfont-*contentunicode-bidiwhite-space
像background、width、height等盒模型相关的样式用在::marker上也是无效的。但::marker的content生成的标记框内容,我们还可以使用text-*相关的属性。比如:
:root {
--content: "»»»";
}
.list-item::marker {
content: var(--content);
color: #f36;
font-size: 1.5rem;
text-shadow: 2px 2px 0px #09f;
text-transform: capitalize;
letter-spacing: 5px;
transition: all 0.2s ease-in;
/* 不生效的 CSS */
background: #909090;
transform: rotate(30deg);
opacity: 0.5;
filter: blur(10px);
width: 50px;
height: 50px;
margin-right: 40px;
}
.list-item:hover::marker {
font-size: 0.9em;
color: lime;
text-shadow: -2px -2px 1px #090909;
}
.list .list-item::marker {
animation: change 3s ease-in-out infinite;
}
@keyframes change {
50% {
color: hotpink;
text-shadow: 1px 1px 1px green;
font-size: 2rem;
}
}
也就是说,我们在::marker伪元素上可控样式还是有限,要实现下面这样的个性化效果是不可能的:

不过,庆幸的是,CSS 中除了::marker伪元素之外,我们还可以使用::before或::after来生成内容,然后通过 CSS 来实现更具个性化的列表标记样式。接下来,我们一起来看看伪元素::before和::after在列表标记符上的使用。
伪元素 ::before 和 ::after
伪元素::before和::after是 CSS Pseudo-Elements Module Level 4 规范中的内容。它们配合 CSS Generated Content Module Level 3 中的content 创建出两个伪元素。这样一来,一个 HTML 元素就具备多个盒模型:

CSS 中的伪元素
::before和::after可以做很多事情,如果你对这方面感兴趣的话,可以阅读《伪元素能帮助我们做些什么》一文!
同样的,伪元素::before和::after也可以用于列表项(li)和 display 为 list-item 的元素中。通过前面的学习,我们知道,列表项有一个标记框(Marker Box),即对应的::marker伪元素生成的盒子。那么问题来了,如果列表项中的::marker和另外两个伪元素::before和::after同时存时,会是什么情况呢?
.list-item::marker {
content: "Marker";
color: #f36;
}
.list-item::before {
content: "Before";
color: #09f;
}
.list-item::after {
content: "After";
color: #89f;
}
可以看到::marker和::before生成的盒子框在列表项内容框的前面,并且::marker在::before前面,而::after生成的盒子框在列表项内容框的后面:

再来看另外一场景,如果伪元素::marker未指定生成的内容,但使用::before生成内容,你将看到下图这样的效果:
.list-item::marker {
color: #f36;
font-size: 2rem;
}
.list-item::before {
content: "Before";
color: #09f;
}

也就是说,如果不希望列表项的标记框存在(如果使用::before来实现标记符样式),需要将::marker的content的值设置为none,或者改变列表项display的值(非list-item即可,但不能是none):
.list-item::marker {
content: none;
}
/* 或 */
.list-item {
display: inline-flex; /*非list-items,但不能是none*/
}

另外,伪元素::before和::after中的content也可以是<string>(字符串,包括空字符串),<image>(图像)等:
:root {
--content: "»";
}
.list > .list-item {
display: list-item;
}
.list-item::marker {
content: none;
}
.list-item::before {
content: var(--content);
}
改变content的值,你可以看到下面这个效果:
既然 ::marker 也能像伪元素 ::before 和 ::after 一样给列表框生成内容,那我们就使用::marker就可以了。当然,如果只是为了生成标记框的内容,这是可以的,但要为列表标记符设置个性化较强的样式网格,那::marker就局限性太大了。在介绍 ::marker 的时候,我们知道可以用于::marker的 CSS 属性有限。但::before和::after就不一样了,一般情况下能用于元素的 CSS 属性都可以用于伪元素::before和::after上。比如上面的示例,使用 CSS 可以给其添加一些个性化的样式效果:
.list-item::before {
content: var(--content);
margin-right: 10px;
display: inline-flex;
justify-content: center;
align-items: center;
color: #fff;
width: 36px;
height: 36px;
background-image: conic-gradient(from 30deg, #ff005e, #cc196c, #ff003b);
background-size: cover;
border: 1px solid rgba(255, 255, 255, 0.125);
backdrop-filter: blur(16px) saturate(180%);
background-color: rgba(17, 25, 40, 0.75);
border-radius: 50%;
font-size: 1.325rem;
text-shadow: 1px 1px 1px #3500ff;
filter: drop-shadow(1px 1px 2px rgb(0 0 0 / 0.25));
}
还可以在伪元素上使用animation,让列表标记符动起来:
.list-item::before {
animation: spin 2.5s ease-in-out infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg) scale(1);
}
50% {
transform: rotate(720deg) scale(0.6);
filter: blur(3px);
}
100% {
transform: rotate(0deg) scale(1);
}
}
效果如下:
使用 ::before 和 ::after 我们还可以实现更复杂,更具个性的列表标记符的效果:
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
margin: 1.25em auto;
border: solid 0.5em transparent;
padding: 0.25em;
width: 100%;
height: 5em;
border-radius: 2.5em;
background: linear-gradient(#dbdbdb, #fff) content-box,
linear-gradient(var(--slist)) padding-box,
linear-gradient(#fff, #dcdcdc) border-box;
font: 1.5em/1.375 trebuchet ms, verdana, sans-serif;
text-align: center;
text-indent: 1em;
clip-path: inset(0 round 2.5em);
}
.list-item::before {
position: absolute;
right: -0.5em;
width: 5em;
height: 5em;
border-radius: 0.5em;
transform: rotate(45deg);
box-shadow: 0 0 7px rgb(0 0 0 / 20%);
background: linear-gradient(
-45deg,
#e4e4e4 calc(50% - 2.5em),
#fff calc(50% + 2.5em)
);
content: "";
}
.list-item::after {
box-sizing: inherit;
display: grid;
place-content: center;
position: relative;
border: inherit;
margin-right: -0.25em;
width: 4em;
height: 4em;
border-radius: 50%;
box-shadow: inset 0 0 1px 1px #efefef, inset 0 -0.5em rgb(0 0 0 / 10%);
background: linear-gradient(var(--slist)) padding-box,
linear-gradient(#d0d0d0, #e7e7e7) border-box;
color: #fff;
text-indent: 0;
content: "❢";
}
我们知道,在content中可以使用counter()将自动生成的计数器值当作content的值,它可以用在::marker、::before和::after的content中:
ul ::marker {
content: counter(list-item, decimal-leading-zero);
color: #09f;
}
ol ::marker {
content: none;
}
ol ::before {
content: counter(list-item, decimal-leading-zero);
color: #09f;
}

但在很多情况之下,为了构建更具个性化的列表标记符的样式风格,会把<li>的display值设置为flex或inline-flex(也有可能是grid或inline-grid),这个时候列表项<li>就不是list-item的上下文格式,在content继续使用counter(list-item) 函数,就无法自动按DOM的数量生成有序计数值:
.list-item {
display: flex;
}
.list-item::after {
content: counter(list-item, decimal-leading-zero);
}

但,counter()第二个参数指定的是符号(Symbolic,大多数无序列表标记符),即值为disc,circle,square,disclosure-open 和 disclosure-closed也是有效的。如果不需要有顺序的要求,那效果还是可以接受的:
.list-item::after {
content: counter(list-item, circle);
}

如果使用了@counter-style自定义列表符标记,在counter()也是可以使用的:

但要自动计数的话,除了使用counter()函数之外,还需要使用counter-reset和counter-increment属性。比如下面这个效果:

接下来的第三部分,将主要和大家探讨自动生成计数器相关的话题。感兴趣的同学,请持续关注后续更新。