CSS 伪类与伪元素

深入理解 CSS 伪类的分类体系(状态、结构、逻辑)、伪元素的本质(实际创建的新元素)、以及 :is() 和 :where() 的特异性差异。

CSS 伪类与伪元素

伪类和伪元素是 CSS 的语法糖

很多人以为伪类和伪元素是 CSS 的"特殊"机制,实际上它们只是选择器的延伸。伪类让我们能够选择元素的特定状态或结构位置,伪元素则允许我们创建和样式化文档中不存在的新内容。

理解伪类和伪元素的本质区别——伪类是选择已有元素的不同状态,伪元素是创建新元素——是避免混淆的关键。

面试定位:伪类和伪元素的区别是 CSS 面试的基础题。面试官还可能问到 :nth-child 的用法、::before 和 ::after 的实际应用、以及 :is() 和 :where() 的特异性差异。


伪类(Pseudo-classes)

伪类的分类体系

1. 状态伪类(User Action Pseudo-classes)

/* 链接相关状态 */
a:link         { }  /* 未访问的链接 */
a:visited     { }  /* 已访问的链接 */
a:hover       { }  /* 鼠标悬停 */
a:focus       { }  /* 获得焦点 */
a:active      { }  /* 激活状态(点击中) */

/* 表单元素状态 */
input:focus        { }  /* 获得焦点 */
input:focus-visible { } /* 键盘焦点(而不是鼠标点击) */
input:disabled      { }  /* 禁用状态 */
input:enabled       { }  /* 启用状态 */
input:checked       { }  /* 选中状态(radio/checkbox) */
input:indeterminate { } /* 不确定状态 */
input:valid         { }  /* 验证通过 */
input:invalid       { }  /* 验证失败 */
input:placeholder-shown { } /* 占位符显示时 */

2. 结构伪类(Structural Pseudo-classes)

/* 关系型 */
:first-child      { }  /* 父元素的第一个子元素 */
:last-child       { }  /* 父元素的最后一个子元素 */
:only-child       { }  /* 父元素的唯一子元素 */
:first-of-type    { }  /* 同类型中的第一个 */
:last-of-type     { }  /* 同类型中的最后一个 */
:only-of-type     { }  /* 同类型中的唯一 */

/* nth-child 系列 */
:nth-child(2)           { }  /* 第二个子元素 */
:nth-child(2n)          { }  /* 偶数子元素 */
:nth-child(2n+1)        { }  /* 奇数子元素 */
:nth-child(-n+3)        { }  /* 前三个 */
:nth-child(n+4):nth-child(-n+8) { } /* 4-8 */

/* 公式示例 */
:nth-child(3n+1)   { }  /* 1, 4, 7, 10... */
:nth-child(3n)     { }  /* 3, 6, 9, 12... */
:nth-child(odd)    { }  /* 奇数 */
:nth-child(even)   { }  /* 偶数 */

/* 其他 */
:nth-last-child(n)     { }  /* 从后往前数 */
:nth-of-type(n)        { }  /* 同类型从前往后 */
:nth-last-of-type(n)   { }  /* 同类型从后往前 */

/* 空元素 */
:empty { }  /* 没有子元素的元素 */

/* 根元素 */
:root { }  /* 文档根元素,等同于 html */

3. 逻辑伪类(Logical Pseudo-classes)

/* not 否定 */
:not(.active) { }  /* 不包含 active 类的元素 */

/* is / where 匹配列表 */
:is(h1, h2, h3) { }      /* 匹配 h1, h2, h3 */
:where(h1, h2, h3) { }   /* 同样匹配 */

/* has 父元素选择器(现代浏览器支持) */
:has(.child) { }   /* 包含 .child 子元素的元素 */
:has(+ .sibling) { } /* 紧接 .sibling 兄弟的元素 */

/* lang 语言 */
:lang(en) { }  /* 语言为英语的元素 */

:nth-child 的深入理解

/* 常见误解:nth-child 选择的是"第 n 个子元素且满足选择器" */
/* 正确理解:nth-child(an+b) 是一个公式,选择位置满足 an+b 的元素 */

/* 例:选择前三个 */
li:nth-child(-n+3) {
  background: lightblue;
}

/* 例:3 的倍数 */
li:nth-child(3n) {
  background: lightgreen;
}

/* 例:选择倒数第二个 */
li:nth-child(-n+2) {
  /* 这会选择第倒数第2和倒数第1个 */
}

伪元素(Pseudo-elements)

伪元素的本质

伪元素创建了文档中不存在的新元素,允许我们为其添加样式而不改变 HTML 结构。伪元素在 DOM 中不是真实元素,但可以被 CSS 选择和样式化。

常用伪元素

/* 1. ::before 和 ::after */
.element::before {
  content: "";  /* 必须设置 content */
}

.element::after {
  content: "→"; /* 可以是空字符串或文本 */
}

/* 2. ::first-line 和 ::first-letter */
p::first-line {
  /* 第一行的样式 */
  text-transform: uppercase;
}

p::first-letter {
  /* 第一个字符的样式 */
  font-size: 3em;
  float: left;
}

/* 3. ::selection */
::selection {
  background: yellow;
  color: black;
}

/* 4. ::placeholder */
input::placeholder {
  color: gray;
  font-style: italic;
}

/* 5. ::marker(列表标记) */
li::marker {
  color: red;
}

/* 6. ::cue(WebVTT 字幕) */
video::cue {
  background: rgba(0, 0, 0, 0.8);
  color: white;
}

::before 和 ::after 的实际应用

/* 1. 清除浮动 */
.clearfix::after {
  content: "";
  display: block;
  clear: both;
}

/* 2. 添加图标 */
.btn-download::before {
  content: "↓";
  margin-right: 8px;
}

/* 3. 引用样式 */
blockquote::before {
  content: """;
  font-size: 3em;
  color: #ddd;
  float: left;
  line-height: 1;
}

/* 4. 渐变遮罩 */
.clamp-text {
  position: relative;
}

.clamp-text::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 2em;
  background: linear-gradient(transparent, white);
}

:is() vs :where() vs :has()

特异性对比

/* :is() — 取括号内最高特异性 */
:is(h1, .title) { }  /* 特异性:0,1,0(class 选择器的特异性) */
:is(#header, h2) { }  /* 特异性:1,0,0(ID 选择器的特异性) */

/* :where() — 始终为 0 */
:where(h1, .title) { }  /* 特异性:0,0,0 */

/* :has() — 括号内选择器的特异性 */
div:has(.child) { }   /* 特异性:0,1,0(class 的特异性) */
div:has(#unique) { }  /* 特异性:1,0,0(ID 的特异性) */

实际应用场景

/* :where() 用于样式覆盖(低特异性) */
.library-button { background: blue; }

/* 你的覆盖样式 */
:where(.library-button) { background: red; }
/* 特异性:0,0,0,不会被 .library-button 的 0,1,0 压制 */

/* :is() 用于简化复杂选择器 */
:is(section, article, aside) :is(h1, h2, h3) {
  /* 匹配 section/article/aside 内的 h1/h2/h3 */
}

/* :has() 用于父元素选择(相对较新) */
.card:has(.badge) { }     /* 有 .badge 子元素的 .card */
.card:has(+ .card) { }   /* 紧接另一个 .card 的 .card */
.card:has(> .featured) { } /* 有 .featured 直接子元素的 .card */

面试高频问题

Q: 伪类和伪元素的区别是什么?

回答要点:伪类是选择已有元素的特定状态或位置(如 :hover、:first-child),元素本身在 DOM 中存在。伪元素是创建新元素(如 ::before、::first-line),虽然 DOM 中不存在,但可以被 CSS 选择和样式化。语法上的区别:伪类用单冒号(:),伪元素用双冒号(::)。

Q: :first-child 和 :first-of-type 有什么区别?

回答要点::first-child 要求元素是其父元素的第一个子元素;:first-of-type 要求元素是其父元素中同类型的第一个。换句话说,:first-of-type 只看同标签的兄弟,:first-child 看所有兄弟。

Q: :is() 和 :where() 有什么区别?

回答要点:两者都接受选择器列表。:is() 的特异性是括号内选择器的最高特异性,:where() 的特异性始终为 0。:where() 用于需要低特异性以便后续覆盖的场景,:is() 用于需要保持选择器特异性的场景。


延展阅读