CSS 22个非前缀伪元素详细解析

819 阅读17分钟
image.png

伪元素(CSS pseudo-element)是文档中除由文档语言显式创建的元素之外的抽象元素。是在选择器中使用的一种特殊选择器,用于选择并添加样式到元素的特定部分,而不是元素本身,可以在不增加HTML结构的情况下,给元素添加额外的视觉效果或内容。

每个伪元素都与原始元素相关联,语法形式为([]表可选):

element::pseudo-element[(argument)] {...}

伪元素与伪类的区别在于,伪元素以双冒号(::)表示,用于选择DOM元素的特定部分或特定状态;而伪类以单冒号(:)表示,用于选择DOM元素的特定状态。

有时候我们会在一些旧代码中看到用单冒号(:)的形式来表示伪元素,这是CSS2中的语法,主要是为了向后兼容,目前大部分情况下还可以使用这种语法,单冒号(:)既可以表示伪类,又可以表示伪元素,但从CSS3开始只推荐使用双冒号(::)来表示伪元素。

注意:一般情况下,在选择器列表中只能使用一个伪元素,并且只能出现在其它非伪元素选择器之后,否则整个规则无效。比如:div::after:hover {...}这是无效的,而div:hover::after {...}是有效的。

目前在MDN中,一共列出了22个伪元素(不包括-moz--webkit-开头的伪元素),本文中浏览器兼容性极差的伪元素将用(❗)进行标志,接下来我们一一介绍它们。

温馨提示:大部分伪元素中只能应用部分样式。

排版伪元素

::first-letter

::first-letter伪元素选择元素中第一个直接子文本节点或元素的首字母、数字或符号(Unicode类别L*N*S*)印刷字符单元及其相关的标点符号应用样式。

image.png

注意上图中,A前面的标点符号也受影响了。在CSS pseudo-element module Level 4(以下简称CPML4)中对此有相应的描述:

前后标点符号也必须作为首字母文本的一部分包含在::first-letter伪元素中.


为了表示相关的前置标点符号,使用了::first-letter的子伪元素::prefix::first-letter::prefix);而相关的后置标点符号则使用了::first-letter的子伪元素::postfix::first-letter::postfix)。但是在Chrome测试时,这没效果。

注意:

  • ::first-letter不会匹配<li><summary>元素前面的标记。
  • MDN: image.png 第二项知道就行,因为大部分浏览器都不兼容: image.png Firfox真的是什么都吃啊。

首字母下沉🌰:

<head>
  <style>
    p {
      width: 120px;
    }

    p::first-letter {
      float: left;
      color: lightblue;
      font-size: 30px;
      padding: 0 3px;
    }
  </style>
</head>

<body>
  <p>测试测试测试测试测试测试测试测试测试测试</p>
</body>

image.png

::first-line

::first-line伪元素选择块级元素的第一行文本应用样式。与::first-letter一样,如果元素的第一个直接子元素不是非文本节点或元素则无效。

第一行受到多种因素影响,可能是动态的,但是无论怎么变,它都只会影响第一个字符到第一行换行前的最后一个字符。

注意:

  • ::first-line中应用背景相关和左对齐样式时,背景相当于是一个行内元素,如果第一行的文本在换行处的字符(比如一个单词)太长并且不能裁断,导致在该字符前换行,从而在第一行末尾出现空位。由于行内元素的宽取决于内容,因此背景可能不会一直延伸到右边距。 image.png
  • 如果元素的第一个子元素是行内块因素,则::first-line伪元素无效(块级元素和行内元素有效):
<head>
  <style>
    div {
      width: 120px;
    }

    div:nth-child(2) span {
      display: inline-block;
    }

    div::first-line {
      color: lightblue;
    }
  </style>
</head>

<body>
  <div>
    <p>测试测试测试测试测试测试测试测试测试测试</p>   <!-- 有效 -->
  </div>
  <div>
    <span>测试测试测试测试测试测试测试测试测试测试</span>   <!-- 无效 -->
  </div>
</body>

image.png

突出显示伪元素

::selection

::selection伪元素表示文档中被选择作为目标或未来某些用户代理操作对象的部分。通常用于更改用户在进行复制等操作时选择的文本部分的默认样式。 image.png

::target-text

::target-text伪元素表示文档URL片段(如果有)直接指定的文本。这需要浏览器支持文本片段(Chrome需要版本80及以上)。

注意:小部分浏览器不兼容该伪类。

文本片段(Text fragments)

来看什么是文本片段,MDN的描述是这样的:

文本片段允许我们直接链接到Web文档中文本的特定部分,而无需作者使用URL片段中的特定语法使用ID对其进行批注。

可以体验一下MDN的🌰:scroll-to-text demo。当点击链接之后,会跳转到demo页面,并且像锚点链接一样滚动到指定的位置,但是这个🌰中没有用到id,实际上其奥秘隐藏在页面链接当中: image.png 这个链接就是使用了文本片段的语法(#之后的部分),它表示访问指定网页中包含特定文本内容(From the foregoing remarks we may gather an idea of the importance)的位置。::target-text就匹配这样的文本

它与锚点链接有点类似,锚点链接是通过引用页面中的锚点来进行导航和定位,而文本片段则是通过匹配页面中的特定文本内容来进行导航和定位。如果页面中不存在对应的ID,锚点链接只会导航到目标页面的顶部;而文本片段则可以用特定的语法指定要链接到的文本内容。

文档片段的语法(主要看#之后的部分,[]表可选):

https://example.com#:~:text=[prefix-,]textStart[,textEnd][,-suffix]

其中:

  • :~:是一个片段指令,告诉用户代理这是一个文本片段,接下来的内容是一个或多个用户代理指令。
  • text=是一个文本指令。用于定义要在链接文档中链接到哪些文本。
  • textStart指定链接文本的起始位置的文本字符串。
  • textEnd指定链接文本的结束位置的文本字符串。
  • prefix-指定链接文本前面应包含哪些文本。当页面上有多个相似的文本时,可以在文本开头加上一些特定的前缀,帮助浏览器选择正确的链接文本。
  • -suffixprefix-同理,可以在文本末尾加上一些特定的前缀

🌰:

<!-- 
  #:~:text=1.-,段落,文本,-片段。
  这匹配除1.之外的其它字符,
  表示匹配前缀为“1.”,从“段落”开始,到“文本”结束,后缀为“片段”的文本
  -->
<p>1.段落,匹配文本片段</p>

如果需要同时匹配多个文本片段可以用&号分隔:

<!-- 
  xxx#:~:text=1.-,第一个段落,文本,-片段&text=2.-,段落,第二个文本,-片段 
  两个p除了1.和2.之外,其它的字符都匹配,并滚动到第一个文本片段匹配的位置
  -->
<p>1.段落,匹配第一个文本片段</p>
<p>2.段落,匹配第二个文本片段</p>

注意:

  • 匹配项不区分大小写。
  • prefix--suffix指定的文本必须紧贴textStarttextEnd指定的文本(空格除外),否则无效。
  • 与锚点链接类似,如果提供的文本片段与目标文档中的任何文本都不匹配,或者用户代理不支持文本片段,则会忽略整个文本片段,只跳转到目标页面顶部。
  • 出于安全考虑,要求链接在一个noopener上下文中打开(即<a href='xxx' rel="noopener">),还需要在window.open()调用中添加noopener
  • 文本片段仅应用于主文档,不会在<iframe>内搜索文本,并且<iframe>导航也不会调用文本片段。

🌰:

<head>
  <style>
    ::target-text {
      background-color: lightpink;
    }
  </style>
</head>

<body>
  <div>
    <h1 id="section1">Section 1</h1>
    <p>1.第一个段落,匹配第一个文本段落</p>
  </div>
  <div>
    <h1 id="section2">Section 2</h1>
    <p>2.第二个段落</p>
  </div>

  <a href="#:~:text=1.-,第一个段落">匹配第一个p元素</a>
</body>

::spelling-error::grammar-error(❗)

两者的浏览器兼容性一致,以::spelling-error为例: image.png

::spelling-error::grammar-error伪元素分别匹配被被用户代理标记为拼写错误语法错误的部分文本。

注意:

CPML4:因为拼写和语法错误的样式可能会泄露有关用户词典内容的信息(其中可能包括用户名,甚至包括其地址簿的内容!实现::spelling-error::grammar-error的UA必须阻止页面读取此类突出显示区段的样式。

🌰:

<head>
  <style>
    ::spelling-error {
      color: lightyellow;
    }

    ::grammar-error {
      background-color: lightypink;
    }
  </style>
</head>

<body>

  <!-- listen to music  -->
  <p>listen mucis</p>
</body>

::highlight()

::highlight()伪元素表示文档中包含或部分包含在具有自定义高亮名称的已注册的自定义高亮的所有范围内的部分(如果有的话)。需要与CSS Custom Highlight API一起使用。

CSS Custom Highlight API

这里简单介绍一下CSS Custom Highlight API:是一种通过Javascript创建指定任意文本范围,并由CSS设置该文本范围的样式,以突出显示文本的API。可以实现在不会影响DOM结构的情况下,指定任意文本,并让其中的每个字符颜色不同、文档查找高亮等等效果。

更多内容详见自定义任意范围高亮 -- CSS Custom Highlight API详解

使用步骤:

  1. 创建范围(Range)对象。
  2. Range创建Highlight对象。
  3. 使用HighlightRegistry注册Highlight对象
  4. 使用::highlight()伪元素设置高光的样式。
<head>
  <style>
    ::highlight(test-highlight) {
      background-color: lightblue;
    }
  </style>
</head>
<body>
  <p>这是第一段很长很长很长很长很长很长很长很长很长很长很长很长的段落</p>
  <p>这是第二段很长很长很长很长很长很长很长很长很长很长很长很长的段落</p>
  <script>
    const p = document.querySelectorAll('p')

    // 如果需要多个范围可以创建多个Range对象,并指定起始和结束范围,相当于分组
    const range = new Range()
    range.setStart(p[0].firstChild, 10);
    range.setEnd(p[1].firstChild, 20);

    /* 
     * 多个Range需要指定相同的高亮时:
     * const highlight = new Highlight(range1, range2...);
     */
     const highlight = new Highlight(range);
    
    // CSS接口的highlights静态属性就是一个HighlightRegistry对象
    CSS.highlights.set('test-highlight', highlight)
  </script>
</body>

image.png

注意:::highlight()和CSS Custom Highlight API的浏览器兼容性都比较差。

遵循DOM树结构的伪元素

::before::after

::before::after伪元素分别用于在目标元素的第一个子节点之前和最后一个子节点之后插入额外的内容。

生成的内容包含在元素的格式化框内,其display默认为inline,会生成一个行内框,与其它行内元素用于参与行内格式化上下文(可以将其理解成是一个行内子元素一样)。因此不适用于替换元素(在渲染过程中用其他内容进行替换的元素,如<img>)或者<br>等元素。

通常情况下,这两个伪元素会与content属性一起使用,因此这里来详细学习一下content属性:

content用于指定在元素或伪元素中呈现的内容。其接受的值类型有很多种,而且不同的值在元素和伪元素中效果可能不一样:

值类型/关键字简介
none在元素中使用没有效果,在伪元素中使用表示不生成伪元素。
normal在元素中使用正常显示元素的内容,在伪元素中使用相当于none
字符串在元素中使用没有效果,在伪元素中使用表示文本内容。
image元素和伪元素都有效,表示通过element()url()image-set()和渐变等函数设置背景。
counter表示counter()counters()函数来显示CSS计数器的值
atr(attribute)在元素中使用没有效果,在伪元素中使用表示将元素的某个HTML属性的值以字符串形式作为文本内容显示。
open-quote/close-quote在元素中使用没有效果,在伪元素中使用表示文本内容为开闭引号。
no-open-quote/no-close-quote在元素中使用没有效果,在伪元素中使用不引入任何内容,但递增(递减)引号的嵌套级别。。
content-list在元素中使用没有效果,在伪元素中使用表示以空格分隔,指定一个或多个上述除none的其它值类型/关键字。
content-list/"Alternative text"在元素中使用没有效果,在伪元素中使用时与content-list类似,后面的"Alternative text"是使用屏幕阅读器等辅助技术时输出的语音文本。

注意:

  • 在伪元素中通过函数引入的图像无法设置其大小,可以改用background的方式设置背景图像。
  • 在将content的值设置为open-quote/close-quote时,close-quote只在::after中有效;并且要在::before中设置了open-quote之后才会显示。

🌰:

<head>
  <style>
    p {、
      width: 120px;
      height: 50px;
    }

    p::after {
      content: url('./images/p1.png') attr(data-test);
    }、
  </style>
</head>

<body>
  <p data-test="123">测试测试测试</p>
</body>

::marker

::marker伪元素表示列表项自动生成的标记框(如<li><summary>等元素前面的标记)。

list-style属性类似,用于改变标记的类型、位置或图片;而::marker可以指定标记的内容和样式,其内容由content属性决定,可以指定各种标点符号和文本字符串等等。或者是改变标记的样式(比如颜色),这是list-style做不到的

注意:

  • 在设置列表项标记时,list-style属性可以由于在<ul>或者<ol>元素中,但是::marker只能在<li>上使用。
  • ::markerlist-style属性无效。

🌰:

<head>
  <style>
    li::marker {
      content: 'abcde';
      color: lightblue;
    }
  </style>
</head>

<body>
  <ul>
    <li>列表项一</li>
    <li>列表项二</li>
    <li>列表项三</li>
    <li>列表项四</li>
  </ul>
</body>

image.png

::placeholder

::placeholder伪元素表示<input><textarea>元素中的占位符文本。值得一提的是,::first-line元素的样式也会应用到占位符文本中,并且优先级比::placeholder高。

🌰:

<head>
  <style>
    textarea::first-line {
      color: lightblue;
    }
    textarea::placeholder {
      color: lightpink;
    }
  </style>
</head>

<body>
  <textarea cols="30" rows="10" placeholder="测试
  测试"></textarea>
</body>

image.png

::file-selector-button

::file-selector-button伪元素表示类型为file<input>元素的按钮部分。

image.png

注意:该伪元素没有什么属性限制。

🌰:

<head>
  <style>
    ::file-selector-button {
      box-sizing: border-box;
      padding: 8px;
      border-radius: 5px;
      border: none;
      outline: none;
      background-color: #409eff;
      color: white
    }
  </style>
</head>

<body>
  <input type="file">
</body>

image.png

::backdrop

::backdrop伪元素表示一个视口大小的框,紧挨着顶层中显示的任何元素的下方单独显示。通常用于在某些情况下覆盖整个视口的背景。比如对话框、弹出窗口、模态框、以及处于全屏模式的元素等等。

🌰:

<head>
  <style>
    .dag::backdrop {
      background-color: rgba(0, 0, 0, .5);
    }
  </style>
</head>

<body>
  <button class="open">显示模态框</button>

  <dialog class="dag">
    <p>这是一个模态框</p>
  </dialog>

  <script>
    let dag = document.querySelector('.dag')
    let open = document.querySelector('.open')
    open.addEventListener('click', () => {
      dag.showModal()
    })
  </script>
</body>

image.png

其它类型伪类

::cue/::cue(selector)::cue-region/::cue-region(selector)(❗)

::cue/::cue(selector)浏览器兼容性良好,但是::cue-region/::cue-region(selector)目前没有相应的浏览器兼容性。

::cue用于匹配与元素相关的WebVTT Node Objects列表。可以应用于匹配元素的构造的任何WebVTT Node Objects列表。可以在WebVTT内部或外部使用

🌰:

WEBVTT 

STYLE
::cue {
  color: lightblue;
}

00:00:03.000 --> 00:00:10.000
hello world!
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    /* 如果是在WebVTT外部使用 */
    /* video::cue {
      color: lightblue;
    } */
  </style>
</head>

<body>
  <video width="500px" controls src="xxx">
    <track default  srclang="en" src="xxx.vtt" />
  </video>
</body>

image.png

在WebVTT中也有一些类似HTML的元素,比如<b><i>等等元素,如果只是想将样式应用到这些元素上时,则可以通过::cue(selector)伪元素来实现,它根据CSS选择器来选择特定的WebVTT内部节点

🌰:

WEBVTT 

STYLE
::cue(b) {
  color: lightblue;
}

00:00:03.000 --> 00:00:10.000
hello <b>world!</b>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    /* 如果是在WebVTT外部使用 */
    /* video::cue(b) {
      color: lightblue;
    } */
  </style>
</head>

<body>
  <video width="500px" controls src="xxx">
    <track default  srclang="en" src="xxx.vtt" />
  </video>
</body>

image.png

::cue/::cue(selector)中,大部分属性都是作用于文本提示元素本身,但是背景类属性除外,它作用在背景盒中,不会应用于其他节点。如果需要将样式应用到文本轨道中的区域(region,可以理解为文本提示的容器),则可以使用::cue-region/::cue-region(selector)伪元素来实现,但是目前没有关于它的浏览器兼容性,因此不过多说明。

::part()::slotted

::part()伪元素在之前的Web Component详细解析有详细介绍过,是给Shadow DOM添加样式的一种方式,需要结合全局HTML partexportparts属性使用,表示Shadow Tree中具有匹配part或者属性的任何元素。这里水就不过多说明。

主要来说明一下::slotted,众所周知,Web Component是有插槽这个概念的,实际跟Vue中的插槽也差不多,而::slotted()表示已放置在HTML模板内的插槽中的任何元素。

注意:::slotted()只有在Shadow DOM中使用才有效。 🌰:

<body>
  <template id="tmpl">
    <p>
      <slot></slot>
    </p>
  </template>

  <!-- 初始 -->
  <div class="box">
    <span>test</span>
  </div>

  <script>
    const box = document.querySelector('.box')
    const tmpl = document.querySelector('#tmpl')

      box.attachShadow({mode: 'open'});
      const style = document.createElement('style')
      style.textContent = `
        ::slotted(span) {
          color: lightblue;
        }
      `

      box.shadowRoot.appendChild(style)
      box.shadowRoot.appendChild(tmpl.content.cloneNode(true));
  </script>
</body>

image.png

View-Transition伪元素

这部分伪元素跟View Transitions API(以下简称VTA)相关,这里只简单介绍一下,详细的可能以后在Web APIs专栏中再说明(如果写的话...),MDN的定义是:

View Transitions API提供了一种机制,用于在不同DOM状态之间轻松创建动画过渡,同时还可以通过单个步骤更新DOM内容。

简单点说就是从一个视图(View)切换到另一个视图的过程,VTA相比传统动画确实可以更轻松的创建出平滑的过渡效果。

可以体验一个简单到爆的🌰: 可以看到,我的逻辑代码都是很平常的内容,只是调用了一下startViewTransition()方法,剩下的平滑过渡它就帮我搞定了。

当然这只是一个十分简单的🌰,VTA能做到的效果远远要比这强得多,只是本文主要是介绍随VTA一起的出现的5个伪元素。它们的结构如下,VTA会使用以下结构构造伪元素树:

image.png

其中:

  • ::view-transition是包含所有视图过渡且位于其它页面内容顶部的根当中,默认是文档根元素。
  • ::view-transition-group()是单个视图过渡的根,用于对过渡视图分组,给不同的元素设置不同的过渡。所有视图过渡都会根据::view-transition进行定位。
  • ::view-transition-image-pair是单个视图过渡在过渡前后(新视图与旧视图)的容器。用于为它的子元素提供隔离,允许图像对与非正常混合模式混合,而不会影响其他视觉输出。
  • ::view-transition-old()是过渡前旧视图的静态屏幕截图快照。可以理解成动画退场过程,并指定退场的细节。
  • ::view-transition-new()是过渡后新视图的实时表示形式。可以理解成动画进场过程,并指定进场的细节。

其中,后四个伪元素的参数由CSS view-transition-name属性决定,用于给元素指定一个视图过渡名称,将元素参与的试图过渡与根视图过渡分开,就是分组。要注意,不能同时有多个元素具有相同名称的view-transition-name

我在测试的时候就踩了一个大坑,我的代码看着没什么问题,结果它报错提示我有意外重复的视图过渡名称,我排查了大半天才发现是我嫌麻烦在元素选择器中使用了view-transition-name,然后我以前下过一个批量下图的浏览器插件,它会自动在页面上插入一段<div>,页面上就有两个<div>导致报错,动画也没效果。我一气之下就把那插件删了。

使用了view-transition-name之后会自动分组,比如有:

figcaption {
  view-transition-name: figure-caption;
}

那么,伪元素树的结构就会变成:

image.png

默认情况下,视图过渡的名称是root,即:

:root {
  view-transition-name: root;
}

🌰:

<head>
  <style>
    .box {
      box-sizing: border-box;
      width: 200px;
      height: 200px;
      view-transition-name: test;
      background-color: lightblue;
    }

    ::view-transition-old(test), 
    ::view-transition-new(test) {
      animation-duration: 5s;
    }
  </style>
</head>

<body>
  <button>切换颜色</button>

  <div class="box"></div>
  <script>
    const btn = document.querySelector('button')
    const box = document.querySelector('.box')

    let flag = 0
    btn.addEventListener('click', (e) => {
      document.startViewTransition(() => {
        if (flag == 0) {
          box.style.backgroundColor = 'lightpink'
          flag = 1
        } else {
          box.style.backgroundColor = 'lightblue'
          flag = 0
        }
      });
    })
  </script>
</body>

注意:那5个伪元素和VTA的浏览器兼容性都比较差。