聊聊CSS的::marker

发布于 大漠

CSS的::marker是一个伪元素,最早接触时间应该是在四年前。四年前在《试探列表中的::marker》一文中就提到过CSS的::marker伪元素,只不过在当时,该伪元素只是大家探索性的在聊该属性,而时至今日却不同,Firefox 68已经追随Safari11.1对该伪元素支持了。也就是说,该伪元素离我们越来越近了,以后我们可以使用::marker帮我们做更多的处理。简而言之,使用::marker伪元素,可以对列表做一些有趣的事情,在本文中,我们将深入的聊聊该伪元素。

初识CSS的::marker

::marker是CSS的伪元素,现在被纳入到CSS Lists Module Level 3规范中。在该规范中涵盖了列表和计算数器相关的属性,比如我们熟悉的list-style-typelist-style-positionlist-stylelist-itemcounter-incrementcounter-resetcounter()counters()等属性。

在CSS中display设置list-item值之后就会生成一个Markers标记以及控制标记位置和样式的几个属性,而且还定义了计数器(计数器是一种特殊的数值对象),而且该计数器通常用于生成标记(Markers)的默认内容。

一时之间,估计大家对于Markers标记并不熟悉,但对于一个列表所涉及到的相关属性应该较为熟悉,对于一个CSS List,它可以涵盖了下图所涉及到的相关属性:

如果你对CSS List中所涉及到的属性不是很了解的话,可以暂时忽略,随着后续的知识,你会越来越清楚的。

解构一个列表

虽然我们在Web的制作中经常会用到列表,但大家可能不会过多的考虑列表相关的属性或使用。就HTML语义化出发,如果遇到无序列表的时候会使用<ul>,遇到有序列表的时候会使用<ol>,但在有些场景(或不追求语义化的同学)会采用其他的标签元素,比如说<div>。针对这个场景,会采用display设置为list-item。如此一来会创建一个块级别的框,以及一个附加的标记框。同时也会自动增加一个隐含列表项计算数器。

ulol元素默认情况之下会带有list-style-typelist-style-imagelist-style-position属性,可以用来设置列表项的标记样式。同样的,带有display:list-item的元素生成的标记框,也可以使用这几个属性来设置标记项样式。

list-style-type的属性有很多个值:

取值不同时,列表符号(也就是Marker标识符)会有不同的效果,比如下面这个示例所示:

在CSS中给列表项设置类型的样式风格可以通过list-style-typelist-style-image来实现,但这两个属性设置列表项的样式风格会有所限制。比如要实现类似下图的列表项样式风格:

值得庆幸的是,CSS的::marker给予我们更大的灵活性,可以让我们实现更多的列表样式风格,而且灵活性也更大。

创建marker标记框

HTML中的ulol元素会自动创建marker标记框。如果通过浏览器调试器来查看的话,你会发现,不管是ul还是ol的子元素li会自带display:list-item属性设计(客户端默认属性),另外会带有一个默认的list-style-type样式设置:

这样一来,它自身就默认创建了一个marker标记框,同时我们可以通过::marker伪元素来设置列表项的样式风格,比如下面这个示例:

ul ::marker,
ol ::marker {
    font-size: 200%;
    color: #00b7a8;
    font-family: "Comic Sans MS", cursive, sans-serif;
}

你会看到效果如下所示:

特别声明:请使用Firefox 68+ 或 Safari 11.1+ 浏览器查看Demo效果。如无特别声明,接下来的示例都是在Firefox68+ 或 Safari 11.1+中的效果

对于非列表元素,可以通过display: list-item来创建Marker标记,这样就可以在元素上使用::marker伪元素来设置项目符号的样式。虽然通过display:list-item在形式上看上去像列表项,但在语义化上并没有起到任何的作用。

如果在非列表项的元素是让display:list-item的标签更具可读性或让读屏软件能识别出是列表项,我们可以借助ARIA相关的特性来实现,有关于这方面详细的介绍可以阅读《ARIA Lists》一文。

不过我们在这篇文章中不是来讨论可访问性的相关问题,而是讨论::marker的特性,在接下来的内容中将会深入讨论::marker。你会发现,在某些情况下,带有display:list-item的元素,使用::marker会让列表标识更具可创造性。

在深入探讨::marker使用之前,大家要知道,元素必须要具备一个Marker标记框,对于非列表项的元素需要显式的使用display:list-item来创建Marker标记框

CSS的display属性是一个非常重要的属性,现在被纳入在CSS Display Module Level 3中。CSS的display属性可以改变任何一个元素的框模型。而且在Level 3规范中给display引用了两个值的语法,比如使用display: inline list-item可以创建一个内联列表项。

如果你想了解更多有关于display属性相关的知识,建议您花一点时间阅读下面几篇文章:

::marker的基本使用

前面的小示例中,其实我们已经领略到了::marker的魅力。在列表项li中,其实已经带有Marker标记框,可以借助::marker伪元素来设置列表标记的样式。

我们先来回忆一下,CSS的::marker还未出现(或者说不支持的浏览器)时,要对列表项设置不同的样式,都是通过li上来控制(看上去继承了li上的样式)。虽然能设置列表样式,但还是具有一定的局限性,灵活度不够大 —— 特别是当列表项标记样式和内容要区分时

CSS的::marker会让我们变得容易的多。从前面的示例中我们可以了解到,::marker伪元素和列表项内容是分开的,正因此,我们可以独立为两个部分设计不同的样式。这在以前的CSS版本中是不可能的(除非借助::before伪元素来模拟,稍后也会介绍这一部分)。比如说,我们更改ullicolorfont-size时也会更改标记的colorfont-size。为了达到两者的区分,往往需要在HTML中做一些结构上的调整,比如列表项用一个子元素来包裹(比如span元素或::before伪元素)。

更了大家更易于理解::marker的作用,我们在上面的示例基础上做一些调整

.box:nth-child(odd) li {
    font-size: 200%;
    color: #00b7a8;
    font-family: "Comic Sans MS", cursive, sans-serif;
}

.box:nth-child(even) ::marker {
    font-size: 200%;
    color: #00b7a8;
    font-family: "Comic Sans MS", cursive, sans-serif;
}

代码中的具体作用不做介绍,很简单的代码,但效果却有很大的差异性,如下图所示:

很神奇吧!在浏览器中查看源码,你会发现使用::marker和未使用::marker的差异性:

虽然::marker易于帮助我们控制标记的样式风格,但有一点需要特别注意,如果显式的设置了list-style-type: none时,::marker标记内容就会丢失不可见。在这个时候,不管是否显式的设置了::marker的样式都将会看不到。比如:

大家是否还记得,在::marker还没有出现之前,要对列表项设置别的标记符,比如Emoji。我们就需要通过别的方式来完成,最为常见的是修改HTML的结构,或者借助CSS伪元素::beforeCSS的content属性,例如下面这个示例:

事实上,CSS的::marker和伪元素::before类似,也可以通过contentattr()一起来控制Marker标记的效果。需要记住,生成个性化Marker标记内容需要做到几下几点:

  • 非列表项li元素需要显式的设置display:list-item (内联列表项需要使用display: inline list-item
  • 需要显式设置list-style-typenone
  • 使用content添加内容(也可以通过attr()配合data-*来添加内容)

来看一个小示例

li::marker {
    content: attr(data-emoji);
}

::marker伪元素自从可以使用content来添加内容之后,让我们可操作的空间更大了。对于列表标记(即,带有Marker标记)的元素再也不需要额外的通过::before伪元素和content来生成标记内容。而且,我们还可以结合计算数器相关的特性,让列表标记可造性空间更大。如果你感兴趣的话,请继续往下阅读。

::marker与计数器的结合

对于无序列表,或者说统一使用同样的标记符,那么::markercontent结合就可以解决。但是如果面对的是一个有顺列表,那么我们就需要用到CSS计数器的相关特性。

先来回忆一下CSS的计数器相关的特性。在CSS中计数器有三个属性:

  • counter-reset:设置一个计数器,定义计数器名称,用来标识计数器作用域
  • counter-set:将计数器设置为给定的值。它操作现有计数器的值,并且只在元素上还没有给定名称的计数器时才创建新的计数器
  • counter-increment:用来标识计数器与实际关联元素范围,可接受两个值,第一个值必须是counter-reset定义的标识符,第二个值是可选值,是一个整数值(正负值都可以),用来预设递增的值

以及两个相关的函数:

  • counter():主要配合content一起使用,用来调用定义好的计数器标识符
  • counters():支持嵌套计数器,如果有指定计数器的当前值,则返回一个表示这些计数器的当前值的串联字符串。counters()有两种形式counters(name, string)counters(name, string, style)。通常和伪元素一起使用,但理论上可以支持<string>值的任何地方使用

一般情况之下,counter-resetcounter-incrementcounter()即可满足一个计数器的需求。

CSS的计数器使用非常的简单。在元素的父元素上显式设置:

body {
    counter-reset: section
}

使用counter-reset声明了一个计数器标识符叫section。然后再需要使用计算器的元素上(一般配合伪元素::before)使用counter-increment来调用counter-reset已声明的计数器标识符,然后使用counter(section)来计数:

h3::before {
    counter-increment: section
    content: "Section " counter(section) ": "
}

下图会更详尽一些,把计数器可能会用的值都罗列出来了,可供参考:

回到我们的列表设置中来。::marker还没有得到浏览器支持之前,一般都是使用CSS的计数器来实现一些带有个性化的有顺序列表,比如下面这样的效果:

也可以借助计数器做一些其他的效果比如:

更为厉害的时,CSS的计数器配合复选框或单选按钮还可以做一些小游戏,比如@una教程中向我们展示的一个效果

@kizmarh使用同样的原理,做了一个黑白棋的小游戏

是不是很有意思,只不过我们这篇文章不是来介绍CSS计数器的。如果你从未接触过CSS计数器相关的特性而且又对这方面知识感兴趣的话,那建议你阅读完下面的内容之后回过头来阅读下面几篇有关于CSS计数器的文章:

有关于CSS计数器相关的特性暂且搁置。我们回到::marker的世界中来。

::marker配合content可以定制个性化Marker标记风格。借助CSS计数器,可以更轻易的构建带有顺序的Marker标记。同样可以让Marker标记和内容分离。更易于实现可定制化的样式风格。

接下来,我们来看一个简单的示例,看看::marker生成的标记符和以往生成的标记符效果上有何差异没。

结果很简单,这里使用的是一个无序列表:

<ul>
    <li>
    Item1
    <ul>
        <li>Item 1-1</li>
        <li>Item 1-2</li>
        <li>Item 1-3</li>
    </ul>
    </li>
    <li>Item2</li>
    <li>Item3</li>
    <li>Item4</li>
    <li>Item5</li>
</ul>

你可以根据自己的爱好来选择标签元素。先来看::beforecontent配合counter()counters()的一个效果:

// counter()
.box:nth-child(1) {
    ul {
        counter-reset: item;
    }
    
    li {
        counter-increment: item;
        
        &::before{
            content: counter(item);
            // ...
        }
    }
}

// counters()
.box:nth-child(2) {
    ul {
        counter-reset: item;
    }
    
    li {
        counter-increment: item;
        
        &::before{
            content: counters(item, '.');
            // ...
        }
    }
}

对于上面的效果,大家可能也猜到了。我们再来看一下::marker的使用:

// counter()
.box:nth-child(3) {
    ul {
        counter-reset: item;
    }
    
    li {
        counter-increment: item;
    }
    
    ::marker {
        content: counter(item);
        // ...
    }
}

// counters()
.box:nth-child(4) {
    ul {
        counter-reset: item;
    }
    
    li {
        counter-increment: item;
    }
    
    ::marker {
        content: counters(item, '.');
        // ...
    }
}

你在Firefox68+ 或Safari11.1+的浏览器查看,可以看到::marker和前面::before效果是一样的:

另外使用::marker还有一特殊之处。不管是列表元素还是设置了display:list-item的非列表元素,不需要显式的使用counter-reset声明计数器标识符,也无需使用counter-increment调用已声明的计数器标识符。它可以直接在::marker伪元素的content中使用counter(list-item)counters(list-item, '.')

但是非列表元素,哪怕是设置了display:list-item,直接在::markercontent中使用counters(list-item, '.')所起的效果和我们预期的有所不同。如果在非列表元素的::markercontent中使用counters()达到我们想要的效果,需要使counter-reset先声明计数器标识符,然后counter-increment调用已声明的计数器标识符(回归到以前::before的使用)。具本的可以看下面的示例代码:

::marker {
    content: counter(list-item);
    padding: 5px 30px 5px 12px;
    background: linear-gradient(to right, #f36, #f09);
    font-size: 2rem;
    clip-path: polygon(0% 0%, 75% 0, 75% 51%, 100% 52%, 75% 65%, 75% 100%, 0 100%);
    border-radius: 5px;
    color: #fff;
    text-shadow: 1px 1px 1px rgba(#09f, .5);
}


.box:nth-child(2n) ::marker {
    content: counters(list-item, '.');
} 

.box:nth-child(3) {
    section {
        counter-reset: item;
    }
    
    article {
        counter-increment: item;
    }
    
    ::marker {
        content: counters(item, '.');
    } 
}

具体效果如下

是不是觉得::marker非常有意思,特别是给元素添加Marker标记的时候。换句话说,就是在定制个性化列表符号时,使用::marker伪元素要比::before之类的较为方便。而且::marker是元素原生的列表标记符(::marker)。

一旦::marker伪元素得到所有浏览器支持之后,我们要让列表标记符和内容分离就会多了一种方案:

  • 调整HTML结构
  • 伪元素::beforecontent
  • 伪元素::markercontent

前面也向大家展示了,::marker也可以像::before一样,借助CSS计数器属性,可以更好的实现有序列表,甚至是嵌套的列表。

小结

尽管::marker伪元素已经纳入到了CSS Lists Module Level 3规范中,和CSS中的list-style以及CSS计数器同在一个范围中。但让文本内容和Marker标记符分离,即采用原生的Marker标记,还是有一段路需要走。虽然从样式的形式上实现类似的效果(内容和标记分离)有一定的方案可以实现,但我们还是应该保持一颗探索和尝试的心。如果你喜欢使用::marker的话,又想让不支持的浏览器有一个同等的视觉效果,我们可以借助@supports()::before方案做为::marker的降级方案。感兴趣的同学,不仿尝试一下。如果你想直接看效果的话,可以点击这里查看