图解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-*
color
direction
font-*
content
unicode-bidi
white-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
属性。比如下面这个效果:
接下来的第三部分,将主要和大家探讨自动生成计数器相关的话题。感兴趣的同学,请持续关注后续更新。