CSS选择器
约 4055 字大约 14 分钟
2026-05-03
1. 基础
选择器优先级
优先级计算规则:(inline, id, class/attr/pseudo-class, element/pseudo-element)
#header .nav li a {} /* (0, 1, 2, 2) = 122 */
div.nav ul li a.active {} /* (0, 0, 2, 4) = 024 */
.nav li.active {} /* (0, 0, 2, 1) = 021 */
.nav .active {} /* (0, 0, 2, 0) = 020 */排序:#header .nav li a > div.nav ul li a.active > .nav li.active > .nav .active
追问解答:
!important可以被更高特异性的!important覆盖,或被源码顺序更后的同级别!important覆盖- 内联样式优先级是 (1, 0, 0, 0),但
!important高于一切 - 相同优先级时,后写的生效(源码顺序)
子选择器 vs 后代选择器
解答:
div > p:只选择 div 的直接子元素 pdiv p:选择 div 内所有层级的 p
追问答案:
<div>
<section>
<p>A</p> <!-- div > p 不选中,div p 选中 -->
</section>
<p>B</p> <!-- 两者都选中 -->
</div>只有 B 会被 div > p 选中,因为它才是 div 的直接子元素。
相邻兄弟 vs 通用兄弟
解答:
h1 + p:选择紧跟在 h1 后面的第一个 p(必须是紧邻)h1 ~ p:选择 h1 后面所有同级的 p
追问答案:
<h1>Title</h1>
<p>First</p> <!-- + 和 ~ 都选中 -->
<p>Second</p> <!-- 只有 ~ 选中 -->
<div></div>
<p>Third</p> <!-- 只有 ~ 选中 -->h1 + p 只选中 First,h1 ~ p 选中 First、Second、Third。
进阶
nth-child 系列问题
:nth-child(3) /* 第 3 个 */
:nth-last-child(3) /* 倒数第 3 个 */
:nth-child(odd) /* 奇数位 */
:nth-child(-n+3) /* 前 3 个 */
:nth-child(n+3) /* 第 3 个开始到末尾 */
:nth-child(3n+1) /* 1, 4, 7, 10... */追问解答:
:nth-child(2n+1) /* 1, 3, 5, 7... */
:nth-child(odd) /* 1, 3, 5, 7... */不完全等价!
当子元素类型混合时:
<div>
<h2>0</h2> <!-- 第 1 个子元素 -->
<p>1</p> <!-- 第 2 个子元素 -->
<p>2</p> <!-- 第 3 个子元素 -->
</div>p:nth-child(odd) /* 选中第 1、3、5...位置且是 p 的元素 → 无匹配 */
p:nth-child(2n+1) /* 同上 → 无匹配 */因为 :nth-child(odd) 要求"是奇数位置的子元素 且 是 p 元素",而第 1 个是 h2。
属性选择器
/* 存在属性 */
[disabled] { opacity: 0.5; }
/* 精确匹配 */
[type="text"] { border: 1px solid #ccc; }
/* 开头匹配 */
[href^="https"] { color: green; }
/* 结尾匹配 */
[href$=".pdf"] { background: url(pdf-icon.png); }
/* 包含匹配 */
[class*="btn"] { cursor: pointer; }
/* 空格分隔的词匹配 */
[class~="active"] { font-weight: bold; }
/* 连字符分隔的值匹配 (常用于 lang 属性) */
[lang|="en"] { quotes: '"' '"'; }场景:自动为外链添加图标
a[href^="http"]:not([href*="mysite.com"])::after {
content: " ↗";
}nth-child vs nth-of-type
<div>
<h2>标题</h2>
<p>第一段</p>
<p>第二段</p>
</div>解答:
| 选择器 | 匹配结果 |
|---|---|
p:nth-child(2) | 第一段(第 2 个子元素且是 p) |
p:nth-of-type(2) | 第二段(所有 p 中的第 2 个) |
:nth-child(2) | 第一段(任意类型第 2 个子元素) |
:nth-of-type(2) | 标题(第 2 个 h2)、第二段(第 2 个 p) |
核心区别:
:nth-child(n):先找第 n 个子元素,再检查类型:nth-of-type(n):先按类型分组,再找第 n 个
:not() 选择器
div:not(.hidden) {}追问解答:
/* 两者等价,但写法不同 */
:not(.a):not(.b) /* 同时不匹配 .a 且不匹配 .b */
:not(.a, .b) /* 不匹配 .a 或 .b(CSS4 语法) */能嵌套:
:not(:not(.a)) /* 等价于 .a */性能影响:
:not()本身不会导致严重的性能问题- 但
:not(*)或:not([复杂选择器])可能影响匹配效率 - 现代 browser 对
:not()做了优化,不必过度担心
:empty 选择器
<div></div> <!-- ✅ 选中 -->
<div> </div> <!-- ❌ 不选中(有空格) -->
<div> <!-- ❌ 不选中(有换行) -->
</div>
<div><!-- comment --></div> <!-- ❌ 不选中(有注释) -->
<div><span></span></div> <!-- ❌ 不选中(有子元素) -->:empty 定义:没有任何子元素(包括文本节点、注释、空白)的元素。
:blank vs :empty:
:empty:严格空,不能有任何内容:blank:可以包含空白字符(空格、换行、制表符)
<div> </div> /* :empty 不选中,:blank 选中 */注意::blank 目前浏览器支持有限。
高级
:has() 选择器
基础用法:
/* 包含 img 子元素的 div */
div:has(> img) { border: 1px solid #ccc; }追问解答:
选中 .active 的前一个兄弟:
.item:has(+ .active) { opacity: 0.7; }子元素超过 3 个的 ul:
ul:has(> :nth-child(4)) { /* 样式 */ }性能影响:
:has()会打破 CSS 的"向下匹配"规则,可能导致样式失效范围的扩大- 浏览器需要追踪更多 DOM 变化
- Chrome 105+ 做了大量优化,限制
:has()的使用范围
为什么 CSS 很久没有父选择器:
- 渲染流水线问题:传统 CSS 从父到子单向匹配,父选择器需要反向遍历
- 性能问题:修改子元素可能触发父元素样式重算,级联影响大
- 复杂性:可能导致循环依赖和无限循环
现代浏览器通过 Style Invalidation 优化解决了这些问题。
:is() vs :where()
:is(article, section) p { color: red; }
:where(article, section) p { color: red; }核心区别:
| 特性 | :is() | :where() |
|---|---|---|
| 优先级 | 取参数中最高的 | 永远为 0 |
| 容错性 | 忽略无效选择器 | 忽略无效选择器 |
优先级示例:
:is(#header, .nav) p { color: red; } /* 优先级:(0,1,0,1) */
:where(#header, .nav) p { color: blue; } /* 优先级:(0,0,0,1) */使用场景:
:is():需要保持优先级时:where():需要"零优先级"覆盖时,如 CSS Reset
容错性:
:is(.a, :unsupported) { } /* 正常解析 .a,忽略 :unsupported */
.a, :unsupported { } /* 整个规则无效! */focus 系列伪类
三者区别:
| 伪类 | 含义 |
|---|---|
:focus | 元素获得焦点时 |
:focus-within | 元素或其子元素获得焦点时 |
:focus-visible | 元素获得焦点,且是键盘导航触发时 |
实现"输入框聚焦时父容器高亮":
.input-group:focus-within {
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}:focus-visible 的 a11y 意义:
- 鼠标点击不会触发
:focus-visible(避免不必要的焦点框) - 键盘导航会触发(帮助用户追踪焦点位置)
- 提升无障碍体验
button:focus-visible {
outline: 2px solid blue;
outline-offset: 2px;
}属性选择器
[disabled] /* 存在 disabled 属性 */
[type="text"] /* type 值为 text */
[href^="https"] /* href 以 https 开头 */
[href$=".pdf"] /* href 以 .pdf 结尾 */
[class*="btn"] /* class 包含 btn */
[lang|="en"] /* lang 为 en 或 en-xxx */~= vs *= 区别:
[class~="btn"] /* 精确匹配空格分隔的词,class="btn primary" 匹配 */
[class*="btn"] /* 子串匹配,class="mybtn" 也匹配 */[lang|="en"] 含义: 匹配 lang="en" 或 lang="en-US"、lang="en-GB" 等(连字符分隔)。
伪元素 vs 伪类
本质区别:
| 类型 | 作用 | 示例 |
|---|---|---|
| 伪类 | 选择元素的某种状态 | :hover, :first-child, :checked |
| 伪元素 | 创建不在 DOM 中的元素 | ::before, ::after, ::first-line |
为什么需要 content:
::before/::after创建的是"匿名替换元素"- 没有
content就没有内容,元素不渲染
伪元素支持的属性:
- 大部分 CSS 属性都可以
- 可以设置
width、height、position等 - 不能用
:hover(但父元素可以) - 伪元素可以拥有伪类:
.element:hover::after
.tooltip:hover::after {
content: attr(data-tip);
position: absolute;
/* ... */
}深度
选择器性能
.container .nav ul li a {} /* 更差 */
.nav a {} /* 更好 */原因:
CSS 选择器是从右到左匹配的!
.container .nav ul li a的匹配过程:- 先找所有
a - 检查父元素是否是
li - 检查父元素是否是
ul - 检查祖先是否是
.nav - 检查祖先是否是
.container - 每一步都可能失败,需要大量回溯
- 先找所有
.nav a的匹配过程:- 先找所有
a - 检查祖先是否是
.nav - 完成
- 先找所有
* 选择器性能差的原因:
*匹配所有元素- 每个元素都需要检查后续选择器
- 导致大量无效匹配
重绘/回流触发:
:hover等状态选择器可能导致频繁重绘:has()可能导致更大范围的重算
CSS 选择器匹配原理
从右到左匹配:
选择器: .container .nav ul li a
匹配流程:
1. 找到页面所有 <a> 元素
2. 对每个 <a>,向上查找父元素 <li>
3. 找到 <li> 后,再向上找 <ul>
4. 找到 <ul> 后,再向上找 .nav
5. 找到 .nav 后,再向上找 .container
6. 全部匹配成功则应用样式为什么从右到左:
- 从左到右需要遍历所有子树
- 从右到左可以从叶子节点快速过滤
- 大部分情况下,右边选择器能过滤掉大部分元素
优化建议:
- 避免深层嵌套选择器
- 尽量使用类选择器而不是标签选择器
- 避免使用
*
:has() 实现原理
为什么这么晚支持:
打破单向匹配规则
- 传统 CSS:父 → 子 → 孙(单向)
:has():子 → 父(反向)
Style Invalidation 问题
- 修改子元素需要通知父元素
- 可能导致大范围样式重算
潜在的无限循环
/* 理论上可能导致问题 */ :has(:focus) :focus { }
浏览器优化:
限制失效范围
- 只在 DOM 变化时重算相关部分
- 使用 Bloom Filter 快速排除
延迟计算
- 不在布局阶段计算
:has() - 分离样式计算和布局
- 不在布局阶段计算
编译优化
- 将
:has()转换为高效的匹配代码 - 缓存匹配结果
- 将
不常见伪类
/* URL hash 对应的元素 */
section:target { background: yellow; }
/* URL: page.html#section1 → section#section1 匹配 */
/* 文档根元素,通常用于 CSS 变量 */
:root {
--primary: blue;
}
/* 当前作用域(在 querySelector 中使用) */
:scope > .item { }
/* 自定义元素已定义 */
my-element:defined { display: block; }
/* 文本方向 */
:dir(rtl) { text-align: right; }
/* 语言 */
:lang(zh-CN) { font-family: "Microsoft YaHei"; }
/* 只读/可写 */
input:read-only { background: #eee; }
/* 数值范围内 */
input[type="number"]:in-range { border-color: green; }
input[type="number"]:out-of-range { border-color: red; }
/* 表单默认值 */
input:default { border: 2px solid blue; }
/* 不确定状态(如 checkbox 半选) */
input:indeterminate { opacity: 0.5; }::selection
::selection {
background: #007bff;
color: white;
}
/* 特定元素的选中样式 */
.code::selection {
background: #28a745;
}能继承吗:不能,必须直接设置在元素上
支持的属性:
colorbackground-color(不是background)text-decorationtext-shadow
@scope 规则
解决的问题:CSS 选择器作用域隔离
/* 传统方式:选择器可能冲突 */
.card .title { }
/* @scope 方式:限定作用域 */
@scope (.card) {
.title { font-size: 1.5em; } /* 只匹配 .card 内的 .title */
}
/* 带下界限制 */
@scope (.card) to (.card-content) {
.title { } /* .card 内但不嵌套在 .card-content 内的 .title */
}vs Shadow DOM:
@scope:轻量级作用域,不隔离 JS- Shadow DOM:完全隔离,包括 JS
场景
Flex 末尾元素问题
一个flex布局的盒子,要求里面的最后一个元素,始终在最末尾。且只有 3 个元素及以上才生效。
方案一(推荐):
.container > :last-child:nth-child(n+3) {
margin-left: auto;
}方案二(使用 :has()):
.container:has(> :nth-child(3)) > :last-child {
margin-left: auto;
}方案三(Grid):
.container {
display: grid;
grid-auto-flow: column;
}
.container:has(> :nth-child(3)) {
grid-template-columns: repeat(auto-fill, minmax(max-content, 1fr));
}中间元素靠右:
/* 第 2 个元素之后都靠右 */
.container > :nth-child(n+2) {
margin-left: auto;
}评分组件
关键点:使用 flex-direction: row-reverse 解决 hover 方向问题
<div class="rating">
<input type="radio" name="rating" id="star5" value="5">
<label for="star5">★</label>
<input type="radio" name="rating" id="star4" value="4">
<label for="star4">★</label>
<input type="radio" name="rating" id="star3" value="3">
<label for="star3">★</label>
<input type="radio" name="rating" id="star2" value="2">
<label for="star2">★</label>
<input type="radio" name="rating" id="star1" value="1">
<label for="star1">★</label>
</div>.rating {
display: flex;
flex-direction: row-reverse; /* 关键! */
justify-content: flex-end;
}
.rating input {
display: none;
}
.rating label {
color: #ccc;
cursor: pointer;
font-size: 24px;
transition: color 0.2s;
}
/* hover 时,当前及后面的星星都亮(因为 reverse 了) */
.rating label:hover,
.rating label:hover ~ label {
color: #ffc107;
}
/* 点击后保持高亮 */
.rating input:checked ~ label {
color: #ffc107;
}原理:
row-reverse让 DOM 顺序和视觉顺序相反~选择器选择的是 DOM 顺序的后面,视觉上的前面
根据子元素数量调整布局
.grid {
display: grid;
gap: 16px;
}
/* 默认:单列居中 */
.grid {
grid-template-columns: 1fr;
justify-items: center;
}
.grid > :only-child {
max-width: 300px;
}
/* 2 个元素:两列 */
.grid:has(> :nth-child(2)):not(:has(> :nth-child(3))) {
grid-template-columns: repeat(2, 1fr);
justify-items: stretch;
}
/* 3-4 个元素:三列 */
.grid:has(> :nth-child(3)):not(:has(> :nth-child(5))) {
grid-template-columns: repeat(3, 1fr);
}
/* 5+ 个元素:四列 */
.grid:has(> :nth-child(5)) {
grid-template-columns: repeat(4, 1fr);
}<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>动态Grid布局示例</title>
<style>
/* 基础样式 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
margin-bottom: 40px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.demo-title {
margin-top: 0;
margin-bottom: 16px;
color: #333;
font-size: 16px;
}
.card {
background: #fff;
padding: 24px 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
text-align: center;
color: #333;
font-weight: 500;
}
/* --- 核心:用户提供的动态Grid CSS --- */
.grid {
display: grid;
gap: 16px;
}
/* 默认:单列居中 */
.grid {
grid-template-columns: 1fr;
justify-items: center;
}
.grid > :only-child {
max-width: 300px;
width: 100%;
}
/* 2 个元素:两列 */
.grid:has(> :nth-child(2)):not(:has(> :nth-child(3))) {
grid-template-columns: repeat(2, 1fr);
justify-items: stretch;
}
/* 3-4 个元素:三列 */
.grid:has(> :nth-child(3)):not(:has(> :nth-child(5))) {
grid-template-columns: repeat(3, 1fr);
}
/* 5+ 个元素:四列 */
.grid:has(> :nth-child(5)) {
grid-template-columns: repeat(4, 1fr);
}
/* 动态演示区的按钮样式 */
.btn-group {
margin-top: 16px;
display: flex;
gap: 8px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #1890ff;
color: #fff;
cursor: pointer;
font-size: 14px;
}
.btn:hover {
background: #40a9ff;
}
.btn.danger {
background: #ff4d4f;
}
.btn.danger:hover {
background: #ff7875;
}
</style>
</head>
<body>
<h2>根据子元素数量动态调整的 Grid 布局</h2>
<!-- 演示1:1个元素 -->
<div class="demo-section">
<h3 class="demo-title">1个元素:单列居中,最大宽度300px</h3>
<div class="grid">
<div class="card">首页</div>
</div>
</div>
<!-- 演示2:2个元素 -->
<div class="demo-section">
<h3 class="demo-title">2个元素:两列平分</h3>
<div class="grid">
<div class="card">首页</div>
<div class="card">我的</div>
</div>
</div>
<!-- 演示3:3-4个元素 -->
<div class="demo-section">
<h3 class="demo-title">3-4个元素:三列布局</h3>
<div class="grid">
<div class="card">首页</div>
<div class="card">产品</div>
<div class="card">我的</div>
<div class="card">设置</div>
</div>
</div>
<!-- 演示4:5+个元素 -->
<div class="demo-section">
<h3 class="demo-title">5个及以上元素:四列布局</h3>
<div class="grid">
<div class="card">首页</div>
<div class="card">产品</div>
<div class="card">解决方案</div>
<div class="card">关于我们</div>
<div class="card">我的</div>
<div class="card">设置</div>
<div class="card">帮助</div>
</div>
</div>
<!-- 演示5:动态添加/删除 -->
<div class="demo-section">
<h3 class="demo-title">动态演示:点击按钮添加/删除卡片</h3>
<div class="grid" id="dynamicGrid">
<div class="card">卡片1</div>
</div>
<div class="btn-group">
<button class="btn" onclick="addCard()">+ 添加卡片</button>
<button class="btn danger" onclick="removeCard()">- 删除卡片</button>
</div>
</div>
<script>
let cardCount = 1;
const dynamicGrid = document.getElementById('dynamicGrid');
function addCard() {
cardCount++;
const card = document.createElement('div');
card.className = 'card';
card.textContent = `卡片${cardCount}`;
dynamicGrid.appendChild(card);
}
function removeCard() {
if (cardCount > 0) {
dynamicGrid.removeChild(dynamicGrid.lastChild);
cardCount--;
}
}
</script>
</body>
</html>根据子元素数量应用不同样式
/* 只有 1 个子元素时 */
.list > :only-child { }
/* 正好 3 个子元素时,选中所有 */
.list > :first-child:nth-last-child(3),
.list > :first-child:nth-last-child(3) ~ * {
/* 样式 */
}
/* 超过 5 个子元素时 */
.list > :first-child:nth-last-child(n+6),
.list > :first-child:nth-last-child(n+6) ~ * {
font-size: 12px;
}使用 :has() 实现父选择器
/* 包含图片的卡片 */
.card:has(img) {
padding: 0;
}
/* 包含必填且无效输入的表单 */
form:has(input:required:invalid) {
border-color: red;
}
/* 前一个兄弟选择器 (选中紧邻 .active 之前的元素) */
.item:has(+ .active) {
opacity: 0.7;
}Grid/Flex 布局中处理不同数量
/* Grid: 少于 4 个元素时居中 */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, 100px);
}
.grid:has(:nth-child(4)) {
justify-content: start;
}
.grid:not(:has(:nth-child(4))) {
justify-content: center;
}输入框状态组合
/* 空的必填输入框 */
input:required:placeholder-shown {
border-color: orange;
}
/* 有值且有效的输入框 */
input:not(:placeholder-shown):valid {
border-color: green;
}
/* 聚焦时父容器高亮 */
.input-group:focus-within {
box-shadow: 0 0 0 2px blue;
}列表项之间有分隔线,首尾不要
需求:A | B | C | D,不是 | A | B | C | D |
/* 方案:给除了第一个之外的元素加左边框 */
li + li {
border-left: 1px solid #ccc;
padding-left: 10px;
}思路:+ 相邻兄弟选择器只会选中"前面有兄弟的元素",自然排除了第一个