图解CSS:奇妙的CSS计数器世界(Part2)

发布于 大漠

奇妙的 CSS 计数器世界 第一部分 主要介绍了使用 list-style(以及它们的子属性list-style-typelist-style-imagelist-style-position)属性给列表项设置列表项标记符样式。除此之外,还可以使用 @counter-style 规则来自定义列表项标记符(list-style-type的值)。事实上,每个列表顶的标记符都是::marker伪元素生成的标记框的内容。在这部分中,我们开始进入::marker伪元素世界。感兴趣的请继续往下阅读。

::marker 伪元素

在 CSS 中,li 元素和使用displaylist-item的元素都会有一个标记框(Marker Box),对应的就是::marker伪元素。列表项或displaylist-item的元素其标记符的特征(即标记符样式)是由这个标记框的样式决定的,它是一个符号或序列号。默认情况之下,无序列表的标记符是一个符号,而有序列表的标记符是一个序列号。

注意,display 除了list-item会生成列表项标记框之外,还可以是inline list-itemblock list-item,有关于 display 更详细的介绍,可以阅读《display属性》一文。

在 CSS 布局模型中,列表项的标记由一个与每个列表项相关的标记框表示:

这个标记的内容可以通过列表项上的list-style-typelist-style-image属性以及::marker伪元素分配属性来控制。虽然,使用 list-style-image 可以引用图片来调转标记符样式或使用list-style-type使用字符以及结合@counter-style自定义的标记符给列表标记符设置样式。但他们有一定的局限性,比如要控制他们的位置,个性化的样式等。但我们可以使用::marker伪元素content定义列表项标记符,并且可以很好的对其样式做个性化方面的处理。比如说,只改变列表项标记符的 颜色大小位置 等,甚至还可以构建个性化非常强的标记符效果。

CSS Lists and Counters Module Level 3规范第一部分内容就是专门介绍::marker伪元素的!简单地说,列表项的标记框(Marker Box)是由一个列表项的::marker伪元素生成的,而且作为该列表项的第一个子元素。

列表项标记框的内容

试着回忆一下,默认的列表项和displaylist-items的元素,::marker对应的内容是什么?这里指的是其计算之后的内容。比e如,在列表项上未显式使用 CSS 的 list-style-typelist-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-typelist-style-image属性值,这个时候:

  • 无序列表的列表项(li)的 list-style-typedisclist-style-imagenone
  • 有序列表的列表项(li)的 list-style-typedecimallist-style-imagenone
  • displaylist-item的元素,和无序列表项的表现相似,即list-style-typedisclist-style-imagenone

在此基础上,我们将调整 规则①。即,在::marker伪元素的content指定不同的值:

:root {
    --content: normal;
}

.list-item::marker {
    content: var(--content);
}

content的值运用了上一部分中提到的content可用值,最终效果如下:

::markercontent的值为normal时,标记框的内容则是列表项list-style-type的默认值:

::markercontent的值设置为none时,这个时候标记框会被移除,相当于display: none

::markercontent的值设置为inherit时,它的表现将和content取值为normal相同。

::markercontent的值设置为空字符串(" "),列表项标记框的内容为空字符串,并且将覆盖list-style-type的默认样式。虽然标记框内容为空,但::marker不会被移除:

::markercontent的值设置为其他格式,比如文本字符串,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);
}

::markercontent值为normal时,标记框的内容是list-style-image指定的图片:

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

这也验证了前面介绍list-style-image所说的:

list-style-image 生效需要确保列表项的 ::markercontentnormal!

::markercontent值为none时,标记框会被移除,同时list-style-imagelist-style-type都将失效,同时::marker也类似于display: none。标记框没了,所以标记符样式也将丢失:

::markercontent值为inherit时,表现效果和normal等同,标记框的内容是list-style-image指定的内容。

::markercontent值是其他值类型,比如文本字符,表情符号,图像等时,标记框的内容将会是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);
}

::markercontent值为normal时,标记框的内容是由list-style-type属性指定的值来生成:

::markercontent值为none时,::marker会从DOM中移除,类似于display: none。标记框没有任何内容:

::markercontent值为inherit时,它的表现形式和content取值为normal相同。

同样的,当::markercontent值是其他值类型,比如文本字符,表情符号,图像等时,标记框的内容将会是content中指定的内容,同时也将覆盖list-style-type指定的内容:

最后一种情况,规则②和规则③的值都改变,即list-style-typelist-style-image 属性显式设置值。在此条件之下,同样调整规则①。即,在::marker伪元素的content指定不同的值:

:root {
    --content: normal;
}

.list-item {
    list-style-type: "±";
    list-style-image: url(./rocket.svg);
}

.list-item::marker {
    content: var(--content);
}

::markercontent 值为 normal 时,标记框的内容是由 list-style-image 引入的图像资源来决定,但当 list-style-image 引入的图片资源失效时,标记框的内容是 list-style-type 属性的值。

这也验证了:

list-style-type生效时,::markercontent值为normal,且list-style-imagenone !

::markercontent值为none时,::marker会从DOM中移除,类似于display: none。标记框没有任何内容。

::markercontent值为inherit时,它的表现效果和content取值为normal相同。

::markercontent值是其他值类型,比如文本字符,表情符号,图像等时,标记框的内容将会是content中指定的内容,同时也将覆盖list-style-typelist-style-image 指定的内容。

简单地总结一下:

  • ::markercontent值为none时,::marker伪元素会从DOM中移除,相当于display: none,此时标记框不会有任何内容,即使是list-style-typelist-style-image设置了值也是如此
  • ::markercontent值为normal时,且list-style-imagelist-style-type未显式设置值时,此时标记框的内容将会是list-style-type的默认值
  • ::markercontent值为normal时,且list-style-image显式设置了值,但list-style-type并未显式设置值,此时标记框的内容将会是list-style-image的值,要是list-style-image的图像资源失效时,将会取list-style-type的默认值作为标记框的内容
  • ::markercontent值为normal时,且list-style-type显示设置了值,但list-style-image并未显式设置值,此时标记框的内容将会是list-style-type的值
  • ::markercontent值为normal时,且list-style-imagelist-style-type显式设置了值,此时标记框的内容将会是list-style-image的值,但当list-style-image引入的资源失效时,标记框的内容会是list-style-type的值
  • ::markercontent值为inherit时,标记框的内容和contentnormal相同
  • ::markercontent值为非normalnoneinherit的其他数据类型,比如文本字符、表情符号、引号、图形等,即使是list-style-typelist-style-image显式设置值也会被覆盖,此时标记框的内容会是content的值,即使是空字符串(" ")也将生效,只是此时标记框的内容是空字符串,标记符无任何样式形式

特别声明,在::markercontent生成标记框内容,还可以使用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伪元素的时候,这是不太可能的。因此,如果你在ulol)或 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

backgroundwidthheight等盒模型相关的样式用在::marker上也是无效的。但::markercontent生成的标记框内容,我们还可以使用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::afterCSS Pseudo-Elements Module Level 4 规范中的内容。它们配合 CSS Generated Content Module Level 3 中的content 创建出两个伪元素。这样一来,一个 HTML 元素就具备多个盒模型

CSS 中的伪元素 ::before::after 可以做很多事情,如果你对这方面感兴趣的话,可以阅读《伪元素能帮助我们做些什么》一文!

同样的,伪元素::before::after也可以用于列表项(li)和 displaylist-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来实现标记符样式),需要将::markercontent的值设置为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::aftercontent中:

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值设置为flexinline-flex(也有可能是gridinline-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,大多数无序列表标记符),即值为disccirclesquaredisclosure-opendisclosure-closed也是有效的。如果不需要有顺序的要求,那效果还是可以接受的:

.list-item::after {
    content: counter(list-item, circle);
}

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

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

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