第7章 组合选择器与伪类

第七章:组合选择器与伪类

如果说基础选择器是"单兵作战",那组合选择器就是"联合作战"。学会了组合选择器,你就能选中那些"爸爸的儿子的孙子"——精确到骨头里去。而伪类呢,就是给元素加上各种"状态滤镜":第一个孩子要表扬、鼠标悬停的要高亮、选中的要打勾… CSS 伪类就是来干这个的。

7.1 组合选择器

7.1.1 后代选择器(空格)——div p,选中 div 内所有 p

后代选择器(Descendant Selector)用空格连接两个选择器,表示"选中第一个选择器的后代中符合第二个选择器的所有元素"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* 选中所有在 article 里面的 p 标签 */
article p {
  line-height: 1.8;
  margin-bottom: 15px;
}

/* 选中所有在 .sidebar 里面的链接 */
.sidebar a {
  color: #3498db;
  text-decoration: none;
}

.sidebar a:hover {
  text-decoration: underline;
}

/* 选中所有在 nav 里面的 ul 里面的 li */
nav ul li {
  display: inline-block;
  padding: 10px 15px;
}
1
2
3
4
5
6
7
8
9
<!-- HTML 结构示例 -->
<article class="blog-post">
  <h2>文章标题</h2>
  <p>第一段内容,<strong>这段是粗体</strong>,也是后代。</p>
  <div class="quote">
    <p>引用段落,也是后代。</p>
  </div>
  <p>第二段内容。</p>
</article>
1
2
3
/* article p 会选中所有这3个 p 标签 */
/* 因为它们都是 article 的后代 */
/* 不管嵌套多深都行 */

后代选择器的应用场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 1. 文章内容样式 */
.article-content p {
  font-size: 16px;
  line-height: 1.8;
  color: #333;
  margin-bottom: 1em;
}

.article-content h2 {
  font-size: 24px;
  color: #2c3e50;
  margin-top: 2em;
  margin-bottom: 0.5em;
}

.article-content ul {
  padding-left: 25px;
  margin-bottom: 1em;
}

.article-content li {
  margin-bottom: 0.5em;
}

/* 2. 卡片内的标题和内容 */
.card-title {
  font-size: 20px;
}

.card-content p {
  font-size: 14px;
  color: #666;
  line-height: 1.6;
}

7.1.2 子选择器(>)——div > p,只选中直接子元素 p

子选择器(Child Selector)用 > 连接,表示"只选中直接子元素",不包含孙子及更深层的后代。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* 后代选择器(空格):选中所有后代 */
.article-content p {
  color: #333;
}

/* 子选择器(>):只选中直接子元素 */
.article-content > p {
  font-size: 18px;
  font-weight: bold;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!-- HTML 结构 -->
<article class="article-content">
  <p>直接子元素 p ← 子选择器会选中这个</p>

  <div class="wrapper">
    <p>孙子元素 p ← 子选择器不会选中这个!</p>
  </div>

  <div>
    <div>
      <p>曾孙子元素 p ← 子选择器更不会选中!</p>
    </div>
  </div>

  <p>另一个直接子元素 p ← 子选择器会选中这个</p>
</article>

子选择器的应用场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* 1. 只选中第一层的直接列表项 */
.nav > ul > li {
  display: inline-block;
}

/* 不包括嵌套在 li 里面的 ul 中的 li */

/* 2. 表单的直接子输入框 */
.form > input {
  border: 2px solid #3498db;
}

/* 不包括 form 里面 div 里面的 input */

7.1.3 相邻兄弟选择器(+)——h1 + p,选中 h1 后紧跟的 p

相邻兄弟选择器(Adjacent Sibling Selector)用 + 连接,表示"选中紧跟在第一个选择器后面的那个元素"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* 选中紧跟在 h1 后面的 p */
h1 + p {
  font-size: 18px;
  color: #666;
  margin-top: 10px;
}

/* 选中紧跟在 .intro 后面的段落 */
.intro + p {
  color: #2c3e50;
  font-weight: 500;
}
1
2
3
4
5
6
7
8
<h1>文章标题</h1>
<p>← 相邻兄弟选择器会选中这个 p</p>  <!-- 紧跟在 h1 后面 -->

<p>← 相邻兄弟选择器不会选中这个 p</p>  <!-- 没有紧跟 h1 -->

<div>中间插了一脚</div>

<p>← 相邻兄弟选择器不会选中这个 p</p>  <!-- h1 和 p 之间隔了 div -->

相邻兄弟选择器的实际应用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* 1. 文章标题和正文的间距 */
h1 + p {
  margin-top: 20px;
  font-size: 1.2em;
  color: #666;
}

/* 2. 列表项之间的分隔线 */
.list-item + .list-item {
  border-top: 1px solid #eee;
  margin-top: 10px;
  padding-top: 10px;
}

/* 3. 第一个输入框不需要上边距 */
.input-group + .input-group {
  margin-top: 15px;
}

7.1.4 通用兄弟选择器(~)——h1 ~ p,选中 h1 后所有 p

通用兄弟选择器(General Sibling Selector)用 ~ 连接,表示"选中第一个选择器后面所有符合条件的兄弟元素"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* 选中 h1 后所有的 p */
h1 ~ p {
  color: #666;
  line-height: 1.6;
}

/* 选中 .title 后面所有的 .item */
.title ~ .item {
  background: #f9f9f9;
  padding: 10px;
  margin-bottom: 5px;
}
1
2
3
4
5
6
<h1>大标题</h1>
<p>← 通用兄弟选择器会选中这个</p>  <!-- 紧跟 -->
<p>← 通用兄弟选择器会选中这个</p>  <!-- 隔一个也选 -->
<p>← 通用兄弟选择器会选中这个</p>  <!-- 隔 N 个也选 -->
<div>不是 p,不会被选中</div>
<p>← 通用兄弟选择器会选中这个</p>  <!-- 还是选 -->

组合选择器对比总结:

选择器语法选中范围
后代div pdiv 里面所有 p(包含孙子)
子div > pdiv 的直接子元素 p
相邻兄弟h1 + ph1 后面紧跟的 p
通用兄弟h1 ~ ph1 后面所有 p
graph LR
    A["组合选择器"] --> B["后代(空格)"]
    A --> C["子(>)"]
    A --> D["相邻兄弟(+)"]
    A --> E["通用兄弟(~)"]

    B --> B1["所有后代"]
    C --> C1["只有直接子元素"]
    D --> D1["紧跟的下一个"]
    E --> E1["后续所有兄弟"]

7.2 结构性伪类

7.2.1 :root——文档根元素(HTML 中是 html),权重高于 html 选择器,常用于定义全局 CSS 变量

:root 是一个超级有用的伪类,它选中文档的根元素——在 HTML 中就是 <html> 标签。但是它的权重比直接写 html {} 更高,所以用它来定义 CSS 变量是最佳实践。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* 用 :root 定义全局 CSS 变量 */
:root {
  /* 颜色变量 */
  --primary-color: #3498db;
  --secondary-color: #2ecc71;
  --danger-color: #e74c3c;
  --warning-color: #f39c12;

  /* 字体变量 */
  --font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
  --font-size-base: 16px;
  --line-height: 1.6;

  /* 间距变量 */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;

  /* 圆角变量 */
  --border-radius-sm: 4px;
  --border-radius-md: 8px;
  --border-radius-lg: 16px;

  /* 阴影变量 */
  --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
  --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.2);
}

/* 使用变量 */
.button {
  background-color: var(--primary-color);
  font-family: var(--font-family);
  padding: var(--spacing-md);
  border-radius: var(--border-radius-md);
  box-shadow: var(--shadow-sm);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* :root vs html 的权重对比 */

/* html 标签选择器,权重 = 1 */
html {
  font-size: 16px;
  color: #333;
}

/* :root 伪类选择器,权重 = 0-1-0(高于普通元素选择器) */
:root {
  font-size: 18px;  /* 会覆盖上面的 16px,因为伪类权重更高 */
  color: #666;       /* 会覆盖上面的 #333 */
}

7.2.2 :first-child——第一个子元素

:first-child 选中父元素下的第一个子元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* 选中每个列表的第一个项 */
li:first-child {
  font-weight: bold;
  color: #3498db;
}

/* 选中每个 .card 的第一个子元素 */
.card:first-child {
  border-top-left-radius: 12px;
  border-top-right-radius: 12px;
}

/* 选中 article 里面第一个 p */
article p:first-child {
  font-size: 18px;
  font-weight: 500;
}
1
2
3
4
5
<ul>
  <li>← :first-child 会选中这个</li>  <!-- 第一个 -->
  <li>第二个</li>
  <li>第三个</li>
</ul>

7.2.3 :last-child——最后一个子元素

:last-child 选中父元素下的最后一个子元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 选中每个列表的最后一项 */
li:last-child {
  border-bottom: none;  /* 列表最后一项不加底边 */
}

/* 选中 .menu 的最后一个链接 */
.menu a:last-child {
  color: #e74c3c;  /* 最后一项标红 */
}

/* ⚠️ 这里选中的是"作为父元素最后一个子元素的那个 a",
   而不是".menu 里排在最后的那一个 a"。
   如果 HTML 是 <ul class="menu"><li><a>…</a></li>…</ul>,
   每个 a 都是它所在 li 的最后一个(也是唯一一个)子元素,
   上面这条规则会把所有链接都标红。
   想只改最后一项,应该把结构写成:
   .menu li:last-child a { color: #e74c3c; } */

/* 选中 article 里面最后一个 p */
article p:last-child {
  margin-bottom: 0;  /* 最后一段落不加下边距 */
}

7.2.4 :first-of-type——同类型子元素的第一个

:first-of-type 和 :first-child 类似,但它是选中同类型子元素中的第一个。

1
2
3
4
5
6
7
<div class="container">
  <p>第一个 p ← :first-child 和 :first-of-type 都选中它</p>
  <p>第二个 p</p>
  <span>第一个 span ← :first-of-type 会选中它</span>
  <p>第三个 p</p>
  <span>第二个 span</span>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* :first-child vs :first-of-type 的区别 */

.container > :first-child {
  /* 选中.container的直接子元素中的第一个(不管是什么标签) */
  background: #f0f0f0;
}

.container > :first-of-type {
  /* 选中.container的同类型子元素中的第一个 */
  background: #e0e0e0;
}

7.2.5 :last-of-type——同类型子元素的最后一个

:last-of-type 选中同类型子元素中的最后一个。

1
2
3
4
5
6
7
8
/* 选中每种类型子元素的最后一个 */
p:last-of-type {
  margin-bottom: 0;
}

h2:last-of-type {
  margin-top: 30px;
}

7.2.6 :only-child——父元素中唯一的子元素

:only-child 选中父元素中唯一的一个子元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* 选中只有一个子元素的父容器 */
.card:only-child {
  /* 整个卡片居中 */
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 200px;
}

/* 选中"自己是父元素唯一子元素"的 .avatar-list */
.avatar-list:only-child {
  margin: 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<div class="card">
  <p>← :only-child 会选中这个</p>
  <!-- 只有一个子元素 -->
</div>

<div class="card">
  <p>第一个</p>
  <p>第二个</p>
  <!-- 有两个子元素,:only-child 不生效 -->
</div>

7.2.7 :only-of-type——父元素中同类型唯一的子元素

:only-of-type 选中父元素中同类型唯一的子元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* 选中唯一一张图片 */
article img:only-of-type {
  width: 100%;
  border-radius: 12px;
}

/* 选中唯一一个小标题 */
section h3:only-of-type {
  font-size: 28px;
  color: #3498db;
}

7.2.8 :nth-child()——选中第 n 个子元素

:nth-child() 是最强大的结构性伪类,可以选中第 n 个子元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* 选中第 3 个子元素 */
li:nth-child(3) {
  background: #3498db;
  color: white;
}

/* 选中前 3 个子元素(通过公式)*/
li:nth-child(-n+3) {
  font-weight: bold;
}

/* 选中从第 4 个开始的所有子元素 */
li:nth-child(n+4) {
  color: #999;
}

7.2.8.1 常见写法——:nth-child(2)(第2个)、:nth-child(odd)(奇数)、:nth-child(even)(偶数)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 选中第 2 个 */
li:nth-child(2) {
  background: #2ecc71;
}

/* odd = 奇数 */
li:nth-child(odd) {
  background: #f5f5f5;
}

/* even = 偶数 */
li:nth-child(even) {
  background: #fff;
}

/* 斑马条纹效果 */
tr:nth-child(odd) {
  background: #f9f9f9;
}

tr:nth-child(even) {
  background: #fff;
}

7.2.8.2 公式写法——:nth-child(2n+1)(第1、3、5…个)、:nth-child(3n)(第3、6、9…个)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 2n+1 = 1, 3, 5, 7... (奇数位置)*/
/* 等同于 odd */
li:nth-child(2n+1) {
  font-weight: bold;
}

/* 2n = 2, 4, 6, 8... (偶数位置)*/
/* 等同于 even */
li:nth-child(2n) {
  background: #f0f0f0;
}

/* 3n = 3, 6, 9, 12... (3的倍数)*/
li:nth-child(3n) {
  color: #e74c3c;  /* 每第三个高亮 */
}

/* 3n+1 = 1, 4, 7, 10... (3的倍数加1)*/
li:nth-child(3n+1) {
  padding-left: 0;
}

/* 3n+2 = 2, 5, 8, 11... (3的倍数加2)*/
li:nth-child(3n+2) {
  padding-left: 20px;
}

/* -n+3 = 前 3 个(-0+3, -1+3, -2+3)*/
li:nth-child(-n+3) {
  font-weight: bold;
  color: #3498db;
}

:nth-child() 常用场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* 1. 网格布局的每一行第一个和最后一个特殊处理 */
.grid-item:nth-child(3n+1) {
  /* 每一行的第一个 */
  clear: left;
}

/* 2. 表单奇偶行不同背景 */
.form-row:nth-child(odd) {
  background: #f9f9f9;
}

/* 3. 步骤指示器的进度点 */
.step:nth-child(1) { /* 第一步 */ }
.step:nth-child(2) { /* 第二步 */ }
.step:nth-child(3) { /* 第三步 */ }

/* 4. 删除线效果 */
li:nth-child(3n) {
  text-decoration: line-through;
  opacity: 0.7;
}

7.2.9 :nth-last-child()——从最后一个开始计数

:nth-last-child() 和 :nth-child() 类似,但是从最后一个开始计数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 选中倒数第 1 个 = 最后一个 */
li:nth-last-child(1) {
  border-bottom: none;
}

/* 选中倒数第 3 个 */
li:nth-last-child(3) {
  color: #e74c3c;
}

/* 选中倒数前 3 个 */
li:nth-last-child(-n+3) {
  font-weight: bold;
}

/* 斑马条纹(从后往前数)*/
tr:nth-last-child(odd) {
  background: #f9f9f9;
}

7.2.10 :nth-child vs :nth-of-type——前者从所有子元素计数,后者只计同类型

1
2
3
4
5
6
7
<div class="container">
  <p>第1个p</p>
  <span>第1个span</span>
  <p>第2个p</p>
  <span>第2个span</span>
  <p>第3个p</p>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* :nth-child(2) - 从所有子元素中数第2个 */
.container > :nth-child(2) {
  color: red;  /* 选中"第1个span",因为它是第二个子元素 */
}

/* :nth-of-type(2) - 从同类型子元素中数第2个 */
.container > p:nth-of-type(2) {
  color: blue;  /* 选中"第2个p" */
}

.container > span:nth-of-type(2) {
  color: green;  /* 选中"第2个span" */
}

7.2.11 :empty——没有子元素(也不含文本节点)的元素

:empty 选中完全空白的元素——既没有子元素,也没有文本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* 选中空的占位元素 */
.placeholder:empty {
  display: none;
}

/* 选中没有内容的提示框 */
.alert:empty {
  display: none;
}

/* 选中空的单元格 */
td:empty {
  background: #f5f5f5;
  /* 给空单元格一个特殊背景色 */
}
1
2
3
4
5
6
7
8
<div class="card">
  <!-- :empty 会选中这个 -->
</div>

<div class="card">
  <p>有内容的卡片</p>
</div>
<!-- :empty 不会选中 -->

⚠️ :empty 有一个非常容易踩的坑:空白文本也算内容。

1
2
3
4
5
<div class="a"></div>        <!-- ✅ :empty 命中 -->
<div class="b">
</div>                       <!-- ❌ 不命中:里面有一个换行/缩进形成的空白文本节点 -->
<div class="c"><!-- 注释 --></div>  <!-- ✅ 命中:注释节点不计入 -->
<div class="d"></div>        <!-- 用 CSS 的 content 加内容也不影响 :empty -->

也就是说,把 HTML 写得"好看"(标签换行缩进)就会破坏 :empty。如果必须依赖它,要么把空元素写成一行 <div></div>,要么改用 :has() 或 JS 判断。另外注意:::before/::after 用 content 生成的内容不会让元素变成非空。


7.3 链接与交互伪类

7.3.1 链接四状态——:link(未访问)、:visited(已访问)、:hover(悬停)、:active(按下),LVHA 顺序不能乱

链接有四个状态,对应四个伪类,是前端工程师必须掌握的基础知识。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 链接的四个状态 */

/* :link - 未访问的链接 */
a:link {
  color: #3498db;
  text-decoration: none;
}

/* :visited - 已访问的链接(浏览器记录过的)*/
a:visited {
  color: #9b59b6;  /* 传统上用紫色表示已访问 */
}

/* :hover - 鼠标悬停 */
a:hover {
  color: #2980b9;
  text-decoration: underline;
}

/* :active - 鼠标按下的一瞬间 */
a:active {
  color: #e74c3c;
  transform: scale(0.98);  /* 按下时轻微缩小 */
}

LVHA 顺序法则: 记住这个口诀:LoVe HAte = LiVe HA**te

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 正确顺序::link → :visited → :hover → :active */
a {
  color: #3498db;
}

a:link {
  color: #3498db;
}

a:visited {
  color: #9b59b6;
}

a:hover {
  color: #2980b9;
  text-decoration: underline;
}

a:active {
  color: #e74c3c;
}

/* ⚠️ 错误示范:顺序乱了会导致样式失效 */
/*
a:hover {
  color: red;
}
a:link {
  color: blue;  ← 这个:hover 可能永远没机会生效
}
*/

为什么顺序这么重要?因为 a:link、a:hover、a:visited、a:active 的特异性完全相同(都是一个元素 + 一个伪类),当特异性打平时,就看谁写在后面。上面那段代码里 a:link 写在最后,于是它会盖掉 a:hover,用户永远看不到悬停变红的效果。

按 LoVe HAte 的顺序写(:link → :visited → :hover → :active),越"临时"的状态越靠后,正好符合"越晚出现越容易赢"的规律:

1
2
3
4
a:link    { color: #3498db; }
a:visited { color: #9b59b6; }
a:hover   { color: #2980b9; }   /* 悬停时盖过前两个 */
a:active  { color: #e74c3c; }   /* 按下时盖过所有 */

另外补一个细节::visited 能改的属性极少,浏览器出于隐私考虑只允许它修改颜色相关的少数属性(color、background-color、border-color 等),你写的 font-size、display 之类会被忽略;而且用 JS 也读不到访问历史。

链接样式的现代写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 现代写法:把基础样式写在 a 标签上,然后用伪类覆盖 */
a {
  color: #3498db;
  text-decoration: none;
  transition: color 0.2s ease;
}

a:hover {
  color: #2980b9;
  text-decoration: underline;
}

a:visited {
  color: #8e44ad;
}

a:active {
  color: #c0392b;
}

7.3.2 :focus——获得焦点时(点击输入框、Tab 键聚焦)

:focus 选中获得焦点的元素,比如点击输入框时。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* 输入框获得焦点时的样式 */
input:focus,
textarea:focus {
  border-color: #3498db;
  box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
  outline: none;  /* 去掉浏览器默认的蓝色轮廓 */
}

/* ⚠️ 上面这种 outline: none 是常见的无障碍问题:
   键盘用户靠这个轮廓判断"焦点在哪",直接去掉会让 Tab 导航"看不见光标"。
   如果确实觉得默认轮廓丑,请像下面这样提供一个替代的可见焦点样式
   (用 box-shadow 或 border 变化都可以),而不是彻底删掉。 */
.field:focus {
  outline: none;
  border-color: #3498db;
  box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.35);  /* 用这个代替轮廓 */
}

/* 按钮获得焦点时的样式 */
button:focus {
  outline: 2px solid #3498db;
  outline-offset: 2px;
}

/* 链接获得焦点时的样式 */
a:focus {
  outline: 2px solid #3498db;
  outline-offset: 2px;
}

7.3.3 :focus-visible——只有键盘聚焦时触发,区分鼠标和键盘

:focus-visible 是一个更智能的 :focus——它只在键盘操作(Tab 键)时生效,鼠标点击时不生效。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 键盘聚焦时显示轮廓(对无障碍访问很重要)*/
button:focus-visible {
  outline: 2px solid #3498db;
  outline-offset: 2px;
}

/* 鼠标点击时不显示轮廓 */
button:focus:not(:focus-visible) {
  outline: none;
}

/* 现代 CSS 的简洁写法 */
button:focus {
  outline: none;  /* 默认不显示 */
}

button:focus-visible {
  outline: 2px solid #3498db;  /* 只有键盘聚焦时才显示 */
}

为什么 :focus-visible 很重要?

graph TD
    A[":focus-visible 的价值"] --> B["鼠标用户"]
    A --> C["键盘用户"]

    B --> B1["点击输入框"]
    B1 --> B2["不需要看到轮廓"]
    B2 --> B3[":focus-visible 不触发 ✓"]

    C --> C1["Tab 键导航"]
    C1 --> C2["需要看到焦点在哪"]
    C2 --> C3[":focus-visible 触发 ✓"]

7.3.4 :focus-within——选中包含焦点子元素的父元素

:focus-within 非常实用——当父元素内部有元素获得焦点时,父元素也会被选中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* 当表单内有输入框获得焦点时,整个表单高亮 */
.form-group:focus-within {
  border-color: #3498db;
  background-color: #f8f9fa;
}

/* 导航下拉菜单 */
.nav-item:focus-within .dropdown {
  display: block;  /* 子菜单获得焦点时显示下拉菜单 */
}

/* 输入框组 */
.input-group:focus-within {
  box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
1
2
3
4
5
<div class="form-group">
  <label>用户名</label>
  <input type="text" />
  <!-- 当 input 获得焦点时,.form-group 也会应用 :focus-within 样式 -->
</div>

7.3.5 :target——URL 锚点指向的元素

:target 非常酷——当 URL 中有锚点(#xxx)指向某个元素时,该元素就会应用 :target 样式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 被锚点指向的元素高亮显示 */
:target {
  background-color: #fffacd;
  border-left: 3px solid #ffd700;
  padding-left: 15px;
}

/* 单页应用中切换内容 */
.section {
  display: none;
}

.section:target {
  display: block;  /* 被锚点指向时显示 */
}

.section:target ~ .section {
  display: none;  /* 隐藏它后面的其它区块 */
}

⚠️ 这段"纯 CSS 选项卡"的示例只能算是演示:所有 .section 默认是 display: none,只有被 :target 命中的那个显示;但兄弟选择器只能往后找,所以它没法"隐藏前面已经展示过的区块"。页面刚打开、URL 里还没有锚点时,所有区块都是隐藏的,用户会看到一片空白。生产环境中做选项卡请用 :checked + label、<details>、或者干脆用一点 JavaScript。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- 导航 -->
<nav>
  <a href="#section1">第一部分</a>
  <a href="#section2">第二部分</a>
  <a href="#section3">第三部分</a>
</nav>

<!-- 内容 -->
<section id="section1">第一部分内容</section>
<section id="section2">第二部分内容</section>
<section id="section3">第三部分内容</section>

<!-- 当 URL 是 page.html#section2 时,id="section2" 的元素就会应用 :target 样式 -->

7.4 表单伪类

表单伪类是 CSS 中最实用的选择器之一,可以根据表单字段的状态自动应用不同样式。

7.4.1 :checked——选中的复选框/单选框

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 复选框选中时的样式 */
input[type="checkbox"]:checked {
  background-color: #3498db;
  border-color: #3498db;
}

/* 自定义复选框 */
.checkbox-wrapper {
  display: flex;
  align-items: center;
  cursor: pointer;
}

.checkbox-wrapper input[type="checkbox"] {
  display: none;  /* 隐藏原生复选框 */
}

.checkbox-wrapper .custom-checkbox {
  width: 20px;
  height: 20px;
  border: 2px solid #ddd;
  border-radius: 4px;
  margin-right: 10px;
  transition: all 0.2s;
}

.checkbox-wrapper input[type="checkbox"]:checked + .custom-checkbox {
  background-color: #3498db;
  border-color: #3498db;
}

/* 单选框选中样式 */
input[type="radio"]:checked {
  border-color: #3498db;
}

⚠️ input 上不能使用 ::before / ::after!

表单控件(input、img、br 等)属于替换元素,它们没有可供伪元素插入的"内容区域"。所以像下面这样写是完全无效的:

1
2
3
4
5
6
7
8
9
/* ❌ 无效:不会生成任何内容 */
input[type="radio"]:checked::after {
  content: "";
  display: block;
  width: 10px;
  height: 10px;
  background: #3498db;
  border-radius: 50%;
}

想画出自定义的选中圆点,正确做法是先把原生外观关掉,再用背景/阴影绘制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* ✅ 用 appearance: none 接管绘制 */
input[type="radio"] {
  appearance: none;          /* 关掉系统默认外观(需要时加 -webkit- 前缀) */
  width: 20px;
  height: 20px;
  border: 2px solid #ccc;
  border-radius: 50%;
  transition: background 0.2s, border-color 0.2s;
}

input[type="radio"]:checked {
  border-color: #3498db;
  background: radial-gradient(circle, #3498db 0 5px, transparent 5px);
}

另一种常见的自定义方式是"隐藏原生控件 + 用相邻兄弟 + 选中一个真正的元素来画",也就是上面复选框示例里的思路——关键点是用一个真实存在的元素承载样式,而不是伪元素。

7.4.2 :valid——通过验证的表单字段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* 输入框内容合法时显示绿色边框 */
input:valid {
  border-color: #2ecc71;
  background-color: rgba(46, 204, 113, 0.1);
}

/* 邮箱格式正确时 */
input[type="email"]:valid {
  border-color: #2ecc71;
}

/* URL 格式正确时 */
input[type="url"]:valid {
  border-color: #2ecc71;
}

7.4.3 :invalid——未通过验证的表单字段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* 输入框内容非法时显示红色边框 */
input:invalid {
  border-color: #e74c3c;
  background-color: rgba(231, 76, 60, 0.1);
}

/* 输入框内容为空且必填时(不填写就验证失败)*/
input:invalid:not(:placeholder-shown) {
  border-color: #e74c3c;
}

/* 错误提示:注意不能在 input 上用 ::after(见 7.4.1 的说明),
   正确做法是让提示文字放在一个真实元素里,再用兄弟选择器控制它的显示 */
.field input:invalid:not(:placeholder-shown) + .error-tip {
  display: block;
}
1
2
3
4
5
<!-- 配套 HTML:错误提示紧跟输入框,默认隐藏 -->
<div class="field">
  <input type="email" required placeholder=" " />
  <span class="error-tip">请输入有效的内容</span>
</div>
1
2
3
4
5
6
.error-tip {
  display: none;       /* 默认不显示 */
  color: #e74c3c;
  font-size: 12px;
  margin-top: 4px;
}

💡 :valid / :invalid 依赖 HTML 的原生校验规则:type="email"、required、minlength、pattern、min/max 等。也就是说,用它们做样式之前要先想清楚"这个字段的校验规则到底是什么"——没有加任何约束的普通文本框永远是 :valid。另外 :invalid 有个著名的体验问题:用户刚打开页面什么都没填时,必填项就已经被判为 :invalid,边框全红了。所以实践里常配合 :not(:placeholder-shown),或者用 :user-invalid(较新,只在该字段被用户"动过"之后才生效)。

7.4.4 :required——必填的表单字段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* 必填字段添加星号标记 */
input:required {
  border-left: 3px solid #e74c3c;
}

/* 必填字段聚焦时的高亮 */
input:required:focus {
  border-color: #3498db;
  box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}

7.4.5 :optional——非必填的表单字段

1
2
3
4
5
/* 可选字段使用不同的样式 */
input:optional {
  border-style: dashed;  /* 虚线边框表示可选 */
  border-color: #ccc;
}

7.4.6 :disabled——禁用的表单字段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 禁用状态 */
input:disabled {
  background-color: #f5f5f5;
  color: #999;
  cursor: not-allowed;
  opacity: 0.7;
}

/* 禁用状态的按钮 */
button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
  opacity: 0.6;
}

/* 禁用状态的 select */
select:disabled {
  cursor: not-allowed;
}

💡 两个细节:

  • :disabled 只对支持 disabled 的控件有效(input、button、select、textarea、fieldset、optgroup、option)。给 <div disabled> 写样式是无效的。
  • 把 <fieldset disabled> 加在整组表单上,组内所有控件都会被浏览器判定为 :disabled,并自动获得"不可交互"的行为,比逐个禁用省事得多——需要禁用整块表单时首选这个办法。

7.4.7 :enabled——启用的表单字段(默认)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* 启用状态的字段 */
input:enabled {
  background-color: white;
  cursor: text;
}

/* :enabled 和 :disabled 对比 */
input:enabled {
  border-color: #3498db;
}

input:disabled {
  border-color: #ddd;
  background: #f5f5f5;
}

7.4.8 :placeholder-shown——占位符文字显示时的输入框

前提:元素必须有 placeholder 属性。 没有 placeholder 时,:placeholder-shown 永远不匹配。这在做"浮动标签"时特别关键——很多人想用 :not(:placeholder-shown) 表示"已经输入了内容",但输入框上根本没写 placeholder,于是完全失效。社区里通行的做法是给它加一个空格占位符:

1
2
3
<!-- 关键技巧:placeholder 写一个空格,
     这样 :placeholder-shown 才有意义,又不会真的显示提示文字 -->
<input type="text" placeholder=" " />
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/* 占位符显示时隐藏输入框边框 */
input:placeholder-shown {
  border-color: #ccc;
  background-color: #f9f9f9;
}

/* 占位符显示时隐藏 label */
.input-group:has(input:placeholder-shown) label {
  opacity: 0.5;
}

/* 浮动标签效果 */
.input-group {
  position: relative;
}

.input-group input {
  padding: 20px 16px 6px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

.input-group input:focus {
  border-color: #3498db;
}

.input-group label {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  color: #999;
  transition: all 0.2s;
  pointer-events: none;
}

/* 输入内容时标签上移 */
.input-group input:focus + label,
.input-group input:not(:placeholder-shown) + label {
  top: 8px;
  font-size: 12px;
  color: #3498db;
  transform: none;
}

7.4.9 :read-only——只读字段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* 只读输入框 */
input:read-only {
  background-color: #f5f5f5;
  color: #666;
  cursor: not-allowed;
}

/* contenteditable 的只读状态 */
[contenteditable="true"]:read-only {
  background-color: #f9f9f9;
  outline: none;
}

7.4.10 :read-write——非只读字段(默认)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* 可编辑状态 */
input:read-write {
  background-color: white;
  border-color: #ddd;
}

/* 可编辑时聚焦高亮 */
input:read-write:focus {
  border-color: #3498db;
  box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}

7.4.11 :in-range——值在 min/max 范围内的输入框

1
2
3
4
5
6
/* 值在范围内的输入框 */
input[type="number"]:in-range,
input[type="range"]:in-range {
  border-color: #2ecc71;
  background-color: rgba(46, 204, 113, 0.1);
}
1
2
<input type="number" min="1" max="10" value="5" />
<!-- 当值在 1-10 范围内时,:in-range 生效 -->

7.4.12 :out-of-range——值超出 min/max 范围的输入框

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* 值超出范围的输入框 */
input[type="number"]:out-of-range,
input[type="range"]:out-of-range {
  border-color: #e74c3c;
  background-color: rgba(231, 76, 60, 0.1);
}

/* 超出范围时显示警告(同样不能在 input 上用伪元素,改为控制兄弟元素) */
.range-field input:out-of-range + .range-tip {
  display: block;
}
1
2
3
4
<div class="range-field">
  <input type="number" min="1" max="10" value="15" />
  <span class="range-tip">请输入 1-10 之间的数字</span>
</div>

表单伪类实战示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* 完整的表单验证样式 */
.form-field {
  margin-bottom: 20px;
}

.form-field label {
  display: block;
  margin-bottom: 8px;
  font-weight: 500;
}

.form-field input {
  width: 100%;
  padding: 12px 16px;
  border: 2px solid #ddd;
  border-radius: 8px;
  font-size: 16px;
  transition: all 0.2s;
}

/* 聚焦状态 */
.form-field input:focus {
  border-color: #3498db;
  box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
  outline: none;
}

/* 验证状态 */
.form-field input:valid:not(:placeholder-shown) {
  border-color: #2ecc71;
}

.form-field input:invalid:not(:placeholder-shown) {
  border-color: #e74c3c;
}

/* 必填标记 */
.form-field input:required + .label::after {
  content: " *";
  color: #e74c3c;
}

/* 禁用状态 */
.form-field input:disabled {
  background-color: #f5f5f5;
  cursor: not-allowed;
}

7.5 逻辑伪类

7.5.1 :not()——否定伪类,:not(.active) 选中不含 .active 类的元素

:not() 是 CSS 中最强大的逻辑伪类,它可以对选择器取反——选中"不符合条件"的元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 选中所有非 .active 的按钮 */
button:not(.active) {
  opacity: 0.6;
  cursor: pointer;
}

/* 选中所有非第一个的 li */
li:not(:first-child) {
  border-top: 1px solid #eee;
}

/* 选中所有不是链接的元素 */
:not(a) {
  /* 这里是除了链接以外的所有元素 */
}

/* 组合使用:选中既不是 .primary 也不是 .danger 的按钮 */
button:not(.primary):not(.danger) {
  background: #95a5a6;
}

/* 选中不是第一个也不是最后一个的 li */
li:not(:first-child):not(:last-child) {
  font-style: italic;
}
1
2
3
4
5
<ul>
  <li class="active">第一项 ← :not(.active) 不选这个</li>
  <li>第二项 ← :not(.active) 选中这个</li>
  <li>第三项 ← :not(.active) 选中这个</li>
</ul>

7.5.2 :is()——简化复杂选择器,如 :is(h1, h2, h3) { },匹配任一选择器

:is() 可以把多个选择器合并成一个,简化代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 之前:需要写三遍 */
h1 { font-size: 32px; color: #333; }
h2 { font-size: 28px; color: #333; }
h3 { font-size: 24px; color: #333; }

/* 现在:用 :is() 一行搞定(但通常还是分开写以保持语义) */
:is(h1, h2, h3) {
  color: #333;
  margin-bottom: 15px;
}

/* 如果确实要统一字号 */
:is(h1, h2, h3) {
  font-size: 2em;  /* 相对单位更灵活 */
  color: #333;
}

/* 更复杂的例子 */
:is(.card, .modal, .dialog) h2 {
  font-size: 24px;
  color: #2c3e50;
}

/* 表单元素的统一样式 */
:is(input, textarea, select):focus {
  border-color: #3498db;
  box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}

7.5.3 :where()——同 :is(),但优先级恒为 0,:where() 的特异性始终为零

:where() 和 :is() 用法完全一样,但有一个关键区别:优先级为 0。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* :is() 有正常的优先级计算 */
:is(#header, .sidebar) h1 {
  color: blue;  /* 权重 = ID(100) + 元素(1) = 101 */
}

/* :where() 优先级永远是 0 */
:where(#header, .sidebar) h1 {
  color: red;  /* 权重 = 0 + 元素(1) = 1 */
}

/* 实际应用:给"基础样式"降权,让别人更容易覆盖你 */

/* 方案 A:用 :is(),特异性 = 类选择器的 10 分 */
:is(.card, .panel) h2 {
  margin: 0;
}

/* 方案 B:用 :where(),特异性 = 只算 h2 的 1 分 */
:where(.card, .panel) h2 {
  margin: 0;
}
/* 后者只要写 .my-article h2 { margin: 1em 0 } 就能轻松覆盖,
   因为它有 2 分(类 + 类 + 元素),而方案 B 只有 1 分。 */

/* 实际应用:给第三方组件的样式"套壳",方便业务层覆盖 */
/* 下面这条只有 .my-app 的 10 分,不会因为库里的复杂选择器而失控 */
.my-app :where(.third-party-widget) .title {
  color: #333;
}
graph TD
    A[":is() vs :where()"] --> B[":is()"]
    A --> C[":where()"]

    B --> B1["有正常优先级"]
    B --> B2["按最高选择器计算权重"]
    B --> B3["无法被轻易覆盖"]

    C --> C1["优先级 = 0"]
    C --> C2["始终为零"]
    C --> C3["容易被覆盖"]

    style B fill:#e74c3c,stroke:#333
    style C fill:#2ecc71,stroke:#333

7.5.4 :has()——父选择器,div:has(.child) 选中包含 .child 的 div,是目前功能最强的伪类

:has() 是 CSS 选择器史上最强大的伪类!它实现了真正的"父选择器"功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 选中包含 .child 的 .parent */
.parent:has(.child) {
  background: #f9f9f9;
}

/* 选中包含图片的卡片 */
.card:has(img) {
  border: 2px solid #3498db;
}

/* 选中包含图标的按钮 */
.button:has(.icon) {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* 选中包含错误提示的表单组 */
.form-group:has(.error-message) {
  border-color: #e74c3c;
}

.form-group:has(.error-message) label {
  color: #e74c3c;
}

/* 选中包含选中复选框的行 */
.table-row:has(input[type="checkbox"]:checked) {
  background: rgba(46, 204, 113, 0.1);
}

/* 选中不包含任何子元素的 div */
div:has(> *:first-child:last-child) {
  /* 这会选择只有唯一一个子元素的 div */
}
1
2
3
4
5
6
7
8
<div class="parent">
  <div class="child">← .parent:has(.child) 会选中这个父 div</div>
</div>

<div class="parent">
  <p>没有 .child</p>
</div>
<!-- 这个不会被选中 -->

:has() 的实际应用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 表单验证:必填字段为空时父容器变红 */
.form-group:has(input:invalid:not(:placeholder-shown)) {
  border-color: #e74c3c;
  background-color: rgba(231, 76, 60, 0.05);
}

/* 购物车有物品时显示数量 */
.cart-icon:has(.cart-count) {
  position: relative;
}

.cart-count {
  position: absolute;
  top: -5px;
  right: -5px;
  background: #e74c3c;
  color: white;
  font-size: 12px;
  padding: 2px 6px;
  border-radius: 50%;
}

/* 表格行选中效果 */
.table-row:has(input[type="checkbox"]:checked) {
  background: rgba(52, 152, 219, 0.1);
}

/* 导航当前页面高亮 */
.nav-item:has(a.current) {
  background: #3498db;
}

:has() 的三个使用须知:

  1. 特异性按 :is() 计算——取括号里最高的那个选择器。上面 .nav-item:has(a.current) 的特异性是"两个类"(.nav-item + a.current 里的 .current),所以很容易压过其他规则,写的时候留个心。
  2. 不能嵌套使用——:has() 里面不能再写 :has(),这是规范明确禁止的(会带来性能问题)。递进关系可以用兄弟/后代选择器拆开表达。
  3. 支持情况::has() 现在已进入所有主流浏览器的稳定版(Chrome 105+、Safari 15.4+、Firefox 121+),可以放心用;但在更老的浏览器上整条规则会被忽略,所以用它做纯装饰可以,别把关键功能(比如"展开/折叠")只押在它身上。

💡 顺便说清"父选择器"的边界::has() 能选中"包含某个后代的元素",也能用 :has(+ ...) 让前一个元素根据紧跟在它后面的元素被选中(例如 h2:has(+ p) 表示"后面紧跟着段落的标题"),但它本身无法"跳到前面去选中兄弟"——+ 和 ~ 永远只向后看。更要紧的是,用 :has() 做布局方向的依赖(比如"根据后面的元素改变前面的宽度")很脆弱,这类需求用 JS 更稳。


7.6 语言与方向伪类

7.6.1 :lang()——根据语言属性选中元素,如 :lang(en)、:lang(zh-CN)

:lang() 根据 HTML 元素的 lang 属性来选择元素,非常适合多语言网站。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* 为英文内容设置特殊字体 */
:lang(en) {
  font-family: "Georgia", serif;
  font-style: italic;
}

/* 为中文内容设置特殊样式 */
:lang(zh-CN) {
  font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
  line-height: 1.8;
}

/* 为日文内容设置特殊样式 */
:lang(ja) {
  font-family: "Hiragino Sans", "Yu Gothic", sans-serif;
}

/* 引用符号样式 */
q:lang(en) {
  quotes: '"' '"';
}

q:lang(zh-CN) {
  quotes: '"' '"';
}

q:lang(ja) {
  quotes: "「" "」";
}
1
2
3
<p lang="en">This is an English paragraph.</p>
<p lang="zh-CN">这是一段中文内容。</p>
<p lang="ja">これは日本語の段落です。</p>

7.6.2 :dir()——根据文本方向选中元素,:dir(ltr)(从左到右)、:dir(rtl)(从右到左)

:dir() 根据文本方向选择元素,是 RTL(从右到左)语言(如阿拉伯语、希伯来语)的最佳拍档。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* 从左到右的文字(中文、英文等)*/
:dir(ltr) {
  text-align: left;
}

/* 从右到左的文字(阿拉伯语、希伯来语等)*/
:dir(rtl) {
  text-align: right;
}

/* RTL 布局中的导航 */
:dir(rtl) .nav {
  padding-right: 20px;
  padding-left: 0;
}

/* RTL 布局中的图标翻转 */
:dir(rtl) .icon-arrow {
  transform: scaleX(-1);  /* 箭头图标反转 */
}

/* RTL 布局中的浮动方向 */
:dir(rtl) .float-left {
  float: right;
}

:dir(rtl) .float-right {
  float: left;
}

7.7 伪元素

伪元素和伪类很像,但有一个关键区别:伪元素创建并选中文档中不存在的新元素。伪元素的写法是 ::(两个冒号),但单冒号 : 也能正常工作(历史兼容)。

7.7.1 ::before——元素内容前插入虚拟元素,需配合 content 属性

::before 在元素内容之前插入一个虚拟元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 在链接前加图标 */
a::before {
  content: "🔗 ";
  margin-right: 5px;
}

/* 在标题前加装饰线 */
h2::before {
  content: "";
  display: inline-block;
  width: 4px;
  height: 20px;
  background: #3498db;
  margin-right: 10px;
  vertical-align: middle;
}

/* 在卡片前加引号 */
blockquote::before {
  content: "\"";   /* 或写成单引号的 content: '"'; */
  font-size: 48px;
  color: #3498db;
  font-family: Georgia, serif;
  line-height: 1;
  display: block;
  margin-bottom: 10px;
}

⚠️ 上面那行 content 是最常见的 CSS 语法错误之一:引号(字符串)内部不能直接出现同类型的未转义引号。写 content: """; 会让浏览器把字符串提前结束,整条声明(甚至后续规则)都被丢弃。正确写法只有两种:

1
2
.a { content: "\""; }   /* ✅ 用反斜杠转义双引号 */
.b { content: '"'; }    /* ✅ 外层用单引号包裹 */

顺便记一下 content 的常用值:""(空,用于画装饰)、"文字"、attr(data-x)(读取属性)、counter(name)(计数器)、url(...)、open-quote/close-quote(配合 quotes 使用)。

7.7.2 ::after——元素内容后插入虚拟元素,需配合 content 属性

::after 在元素内容之后插入一个虚拟元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 在按钮后加箭头 */
.btn::after {
  content: " →";
}

/* 清除浮动(经典用法)*/
.clearfix::after {
  content: "";
  display: table;
  clear: both;
}

/* 给必填项加星号 */
label.required::after {
  content: " *";
  color: #e74c3c;
}

/* 给外链加图标 */
a[href^="http"]::after {
  content: " ↗";
  color: #3498db;
  font-size: 12px;
}

7.7.3 ::first-line——元素的第一行,只能用于块级元素

::first-line 选中元素内容的第一行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* 首行特殊样式 */
p::first-line {
  font-weight: bold;
  color: #2c3e50;
}

/* 首行更大字号 */
.intro::first-line {
  font-size: 1.5em;
  font-weight: 600;
}

/* 首行首字母特殊处理 */
p::first-letter {
  font-size: 3em;
  float: left;
  line-height: 1;
  padding-right: 8px;
  color: #e74c3c;
}

7.7.4 ::first-letter——元素的第一个字母,只能用于块级元素

::first-letter 选中元素的第一个字母/汉字,常用于实现"首字下沉"效果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* 首字下沉效果 */
p::first-letter {
  font-size: 3em;
  float: left;
  line-height: 1;
  padding-right: 10px;
  color: #e74c3c;
  font-weight: bold;
}

/* 杂志风格的第一个字母 */
.article-title::first-letter {
  font-size: 4em;
  float: left;
  line-height: 0.8;
  padding-right: 12px;
  color: #3498db;
  font-family: Georgia, serif;
}

/* 首字下沉时的一个实用技巧:让标点不参与"首字母" */
p::first-letter {
  /* 如果段落以中文引号开头,浏览器会把引号和第一个汉字一起算作 ::first-letter */
  font-size: 3em;
  color: #3498db;
}

⚠️ content 不能用在 ::first-letter 上。 只有 ::before、::after、::marker 这几个伪元素支持用 content 生成内容。像下面这样想在首个字符位置插一个图标是无效的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* ❌ 无效:::first-letter 不会渲染 content */
.icon-label::first-letter {
  content: "🔍";
}

/* ✅ 想加图标请用 ::before */
.icon-label::before {
  content: "🔍";
  margin-right: 5px;
}

::first-line / ::first-letter 的另一个限制:它们只能用一部分属性。::first-line 只能用字体、颜色、背景等少数属性(不能改 margin、padding、width 这类影响布局的属性),::first-letter 支持的属性稍多一些(可以 float、设置 margin/padding/border),但同样不能用来改 display。

7.7.5 ::selection——自定义选中文字样式,可设 color 和 background

::selection 选中用户用鼠标选中的文字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* 选中文字高亮 */
::selection {
  background: rgba(52, 152, 219, 0.3);
  color: #2c3e50;
}

/* 暗黑风格的选中效果 */
.dark-theme ::selection {
  background: rgba(255, 255, 255, 0.2);
  color: #fff;
}

/* 品牌色的选中效果 */
.brand-theme ::selection {
  background: #3498db;
  color: white;
}

7.7.6 ::placeholder——自定义占位符样式,需加前缀(::-webkit-input-placeholder 等)

::placeholder 自定义输入框占位符的样式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* 基础占位符样式 */
input::placeholder {
  color: #999;
  font-style: italic;
}

/* 高级占位符样式 */
input::placeholder {
  color: #ccc;
  font-size: 14px;
  font-style: normal;
}

/* 占位符聚焦效果 */
input:focus::placeholder {
  color: #3498db;
  opacity: 0.8;
}

/* 现代浏览器统一支持不带前缀的 ::placeholder,正常只写这一条就够了 */
input::placeholder {
  color: #999;
}

/* 兼容很老的浏览器时,前缀版本要单独写规则,
   不要和上面那条放进同一个选择器列表 ——
   有一个选择器无效,整组规则都会被丢弃 */
::-webkit-input-placeholder {
  color: #999;
}

💡 常见误区:::placeholder 不能和别的选择器合并成一组。下面这样写是错的:

1
2
3
4
5
6
7
8
9
/* ❌ 只要浏览器不认识其中任何一个选择器,整组规则都会被忽略 */
input::placeholder,
input::-webkit-input-placeholder {
  color: #999;
}

/* ✅ 拆成两条独立规则 */
input::placeholder { color: #999; }
input::-webkit-input-placeholder { color: #999; }

另外 ::placeholder 支持的属性也有限(颜色、字体、text-decoration 等),像 padding、border 这类是无效的——它只是占位符文字,不是元素。

7.7.7 ::marker——自定义列表标记的样式,如 li::marker { color: red; }

::marker 自定义列表的标记符号(圆点、数字等)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 改变列表标记颜色 */
li::marker {
  color: #3498db;
}

/* 改变列表标记字体 */
li::marker {
  font-family: "Font Awesome";
  content: "✓ ";
}

/* 有序列表的数字样式(注意:要写在 li 上,ol 本身没有 marker) */
ol > li::marker {
  color: #e74c3c;
  font-weight: bold;
}

/* 导航列表的箭头标记 */
.nav li::marker {
  content: "→ ";
  color: #3498db;
}

⚠️ ::marker 属于列表项,不属于列表容器。 只有 display: list-item 的元素才有标记,所以 ol::marker、ul::marker 都是无效的(ol/ul 本身不生成标记),必须写成 li::marker 或 ol > li::marker。

::marker 能改的属性很有限:只有 color、font 系列(font-size、font-weight、font-family 等)、content、animation/transition、text-combine-upright、white-space 这一类。写 li::marker { background: red } 或 margin-top: 4px 是没有效果的——想完全自由地控制标记样式(比如做成自定义图标并精确调位置),更常见的做法是把 list-style: none 关掉原生标记,再用 li::before 自己画。

7.7.8 ::cue——自定义媒体字幕的样式

::cue 自定义 <track> 元素显示的字幕样式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* 字幕样式 */
video::cue {
  background: rgba(0, 0, 0, 0.8);
  color: white;
  font-family: "Microsoft YaHei", sans-serif;
  font-size: 18px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}

/* 只给某个"声音"(WebVTT 里的 <v Bob> 标签)的字幕换色 */
video::cue(v[voice="Bob"]) {
  color: #ffd700;
}

/* 只给带某个 class 的字幕(WebVTT 里的 <c.yellow> 标签)换色 */
video::cue(.yellow) {
  color: #ffd700;
}

⚠️ ::cue 能改的属性也很少,主要是颜色、背景、字体、text-decoration、text-shadow、opacity 等,padding、border-radius、margin 这类属性写了不生效——想给字幕加圆角底框,得靠 background 配合 text-shadow,或者干脆自己用 JS 叠加一层自定义字幕。

另外,::cue(xxx) 里的参数是匹配WebVTT 提示文本内部标签的选择器(v、c、i、b、u、lang 等),不是任意 CSS 选择器。

7.7.9 ::file-selector-button——文件选择按钮的样式

::file-selector-button 自定义文件选择按钮的样式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* 文件上传按钮样式 */
input[type="file"]::file-selector-button {
  background: #3498db;
  color: white;
  border: none;
  padding: 8px 16px;
  margin-right: 10px;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.2s;
}

input[type="file"]::file-selector-button:hover {
  background: #2980b9;
}

7.7.10 ::backdrop——元素进入全屏时的背景样式,用于 video:fullscreen::backdrop

::backdrop 全屏元素背后的背景样式。

1
2
3
4
5
6
7
8
9
/* 视频全屏时的背景 */
video:fullscreen::backdrop {
  background: black;
}

/* 图片全屏时的背景 */
img:fullscreen::backdrop {
  background: rgba(0, 0, 0, 0.9);
}

💡 ::backdrop 最常用的场景其实不是全屏,而是 <dialog> 弹窗:

1
2
3
4
5
/* 原生 <dialog> 打开时背后的遮罩层 */
dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);   /* 还能做毛玻璃效果 */
}

dialog::backdrop 只在 dialog.showModal() 打开时出现,是"模态遮罩"最省事的实现方式。注意 ::backdrop 本身不是普通元素,它不继承任何样式,也不会被 body 上的样式影响,需要的话用 :root 里的自定义属性传值进去。

💡 ::file-selector-button 是标准名,旧代码里常见的是 ::-webkit-file-upload-button(Chrome/Safari)。要兼容较老的浏览器,两条规则都写上,但记得分开写——放同一个选择器列表里会让整组失效。

伪元素汇总表:

伪元素说明典型用途
::before元素内容前插入加图标、加装饰
::after元素内容后插入加箭头、清除浮动
::first-line第一行(仅块级容器,可用属性有限)首行特殊样式
::first-letter第一个字母(仅块级容器)首字下沉
::selection选中文字(只能改颜色、背景、阴影等)高亮颜色
::placeholder占位符文字(需元素有 placeholder)输入框占位符样式
::marker列表项的标记(写 li::marker,不是 ol::marker)列表符号样式
::file-selector-button文件选择按钮自定义上传按钮
::backdrop全屏/弹窗背后的遮罩层全屏时背景色、dialog 背景
::cue媒体字幕文本字幕样式

⚠️ 伪元素用在替换元素上不生效:input、img、select、textarea、br、video 等都不支持 ::before/::after,因为它们没有"内容区域"可供插入(不过 input、textarea 支持 ::placeholder,video 支持 ::cue)。给 input 加装饰请改用外层容器 + 兄弟元素。

graph LR
    A["伪元素 vs 伪类"] --> B["伪类"]
    A --> C["伪元素"]

    B --> B1[":hover"]
    B --> B2[":first-child"]
    B --> B3[":checked"]
    B --> B4["选中已有元素的状态"]

    C --> C1["::before"]
    C --> C2["::after"]
    C --> C3["::first-line"]
    C --> C4["创建新的虚拟元素"]

本章小结

恭喜你完成了第七章的学习!让我们来回顾一下这章的精华:

核心知识点

小节重点内容
7.1 组合选择器后代(空格)、子(>)、相邻兄弟(+)、通用兄弟(~)
7.2 结构性伪类:root、:first-child、:nth-child()、:empty 等
7.3 链接与交互伪类:link、:visited、:hover、:focus、:focus-within
7.4 表单伪类:checked、:valid、:invalid、:disabled、:placeholder-shown
7.5 逻辑伪类:not()、:is()、:where()、:has()
7.6 语言与方向伪类:lang()、:dir()
7.7 伪元素::before、::after、::first-line、::first-letter 等

选择器分类图

mindmap
  root((CSS 选择器))
    基础选择器
      标签选择器
      类选择器
      ID 选择器
      通用选择器
      属性选择器
    组合选择器
      后代空格
      子>
      相邻兄弟+
      通用兄弟~
    伪类
      结构性:first-child
      交互:hover :focus
      表单:checked
      逻辑:not :has
    伪元素
      ::before
      ::after
      ::first-line

实战建议

  1. 优先使用类选择器:日常开发以类选择器为主
  2. 善用组合选择器:减少 HTML 结构依赖
  3. 掌握 :has():这是目前最强大的选择器
  4. 区分 :focus 和 :focus-visible:对无障碍访问很重要

本章易错点速查

易错点正确做法
a:last-child 被当成"最后一个链接"它选的是"作为父元素最后一个子元素的 a",多半要写 li:last-child a
给 input 写 ::before / ::after替换元素不支持伪元素,用容器 + 兄弟元素,或 appearance: none 自己画
ol::marker / ul::marker标记长在列表项上,写 li::marker
::first-letter { content: "…" }content 只对 ::before、::after、::marker 生效
content: """;字符串里要转义,写成 content: "\"" 或 content: '"'
:focus { outline: none } 直接删轮廓必须给一个替代的可见焦点样式,否则键盘用户"失明"
用 :placeholder-shown 但没写 placeholder加 placeholder=" " 空格占位符
链接伪类顺序乱写按 LVHA(:link → :visited → :hover → :active)排列
<div>\n</div> 以为能命中 :empty换行会形成空白文本节点,必须写成 <div></div>

下章预告

下一章我们将学习选择器优先级,这是 CSS 中最容易被忽略但又极其重要的知识点。理解了优先级,你才能真正掌握 CSS 的层叠规则!

最后修改 September 19, 2026: 更新 (3489033b1)