前端知库

伪类和伪元素有什么区别?

伪类按状态或位置选中已经存在的元素,比如 :hover、:focus-visible、:nth-child(),写一个冒号。伪元素选中元素里的某一部分,或者生成一个文档里没有的盒子,比如 ::before、::first-line、::placeholder,写两个冒号。伪元素必须放在选择器最后,::before 和 ::after 需要 content 才会生成。

参考答案

简答

button:hover 选中的是鼠标悬停时的那个按钮,按钮本身就在文档里,:hover 只是加了一个“处于悬停状态”的条件,这是伪类。p::first-line 选中的是段落的第一行,文档里并没有一个“第一行”元素,这是伪元素。

MDN 的定义是,伪类根据文档树之外的信息来选择元素,比如元素当前的状态;伪元素则用来给元素的某个特定部分设置样式。所以伪类选的还是整个元素,伪元素选的是元素的一部分,或者像 ::before、::after 那样生成一个新的盒子。

写法上,伪类用一个冒号,伪元素用两个冒号。为了兼容旧代码,浏览器仍然接受 :before、:after、:first-line、:first-letter 这四个的单冒号写法,新代码统一写双冒号。

要注意两点:伪元素必须写在选择器最后,p::before:hover 是无效的,想要悬停时改变伪元素应写成 p:hover::before;::before 和 ::after 必须设置 content 才会生成,而且生成的文字读屏软件不一定能读到,重要信息不要只放在里面。

先看两种用法

CSS
/* 伪类:选中处于某种状态或位置的元素 */
li:nth-child(odd) {
  background: #f5f5f5;
}
a:focus-visible {
  outline: 2px solid currentColor;
}

/* 伪元素:选中元素的一部分,或生成新的盒子 */
.required::after {
  content: '*';
  color: red;
}
input::placeholder {
  color: #999;
}

li:nth-child(odd) 选中的是奇数位置的 li,a:focus-visible 选中的是浏览器判断需要显示焦点框的链接,典型情况是用键盘 Tab 聚焦,它们都是真实存在的元素,只是多了位置或状态的条件。

.required::after 在元素内容的最后生成一个星号盒子,DOM 里并没有这个节点,所以 JavaScript 也选不到它。input::placeholder 选中的是输入框里的占位文字那一部分,同样不是一个独立元素。

为什么伪元素只能放在最后

MDN 规定伪元素必须出现在复合选择器的所有其他部分之后,选择器里最后那个元素就是伪元素的“来源元素”。所以 p::first-line 可以,p::first-line > * 无效,因为伪元素不是真实的元素,没有子元素可选。

想在悬停时改变 ::before,条件要加在来源元素上,写成 .item:hover::before。用 headless Chrome 实测,CSS.supports('selector(p::before:hover)') 返回 false,样式表里这条规则被直接丢弃;p:hover::before 能正常解析。

::before 和 ::after 要注意什么

MDN 说明,::before 会生成所选元素的第一个子元素,::after 生成最后一个子元素,它们常用来通过 content 加装饰,比如图标、引号。没有 content,这两个伪元素不会生成,这也是 clearfix 里要写 content: '' 的原因,见《清除浮动有哪几种方法?》。

它们不能用在 img 这类替换元素上,因为替换元素的内容来自外部资源,不受文档样式影响。另外 MDN 不建议用 ::before 添加有意义的内容,读屏软件不能可靠地读到它。

常见追问

:is()、:not()、:has() 是伪类还是伪元素?

是伪类,而且是带括号参数的函数式伪类。它们最终选中的仍然是完整的元素,只是条件由括号里的选择器决定。:has() 的用法见《:has() 怎样根据后代或相邻元素设置样式?》。

伪类和伪元素会影响优先级吗?

会。伪类和类选择器算同一级,伪元素和元素选择器算同一级。:is()、:not()、:has() 自己不计分,按括号里优先级最高的那个选择器算,:where() 则始终为 0。优先级的完整规则见《层叠、优先级、继承和 !important 谁先决定样式?》。