CSS 选择器详解
CSS 选择器详解
CSS 选择器用于选择要样式化的 HTML 元素,是 CSS 的核心概念之一。可以把选择器理解成"一句话描述你想选中谁":越精准的描述越强大,但也越难被覆盖。理解选择器和它背后的优先级规则,是写出可维护样式的地基。
选择器的本质与重要性
浏览器渲染时会把每条 CSS 规则的选择器和页面里的每个元素做匹配,命中的元素就应用对应样式。这带来两个直接影响:
基础选择器
元素选择器:
类选择器:
ID 选择器:
通用选择器:
属性选择器:
组合选择器
后代选择器:
子选择器:
相邻兄弟选择器:
通用兄弟选择器:
分组选择器:
伪类选择器
伪类描述元素的"状态"或"位置",用单冒号 `:`。
链接与用户行为伪类:
表单伪类:
结构/位置伪类:
逻辑伪类:
伪元素选择器
伪元素创建或选中"文档树里不存在的部分",用双冒号 `::`。
`::before` / `::after`:
`::first-letter` / `::first-line`:
`::selection`:
`::placeholder`:
`::marker`:
优先级(特异性)
优先级(Specificity)决定"多条规则都命中同一元素时,哪条生效"。它是一个可以理解为 (a, b, c) 三元组的计算,逐位比较。
计算规则(数值仅为直观记忆,实际是逐段比较):
示例:
重要规则:
代码示例
基础选择器
/* 元素选择器 */
p { color: #333; line-height: 1.6; }
h1 { font-size: 2rem; font-weight: 700; }
/* 类选择器(主力) */
.button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button--primary { background-color: #007bff; }
.button--secondary { background-color: #6c757d; }
/* ID 选择器(慎用) */
#header {
background-color: #333;
color: white;
padding: 20px;
}
/* 通用选择器:常用于重置 */
* { box-sizing: border-box; }
/* 属性选择器 */
input[type="text"] {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
a[href^="https"] { color: #007bff; } /* 外链 */
a[href$=".pdf"]::after { content: ' (PDF)'; } /* PDF 链接加标记 */
img[alt*="logo"] { border: 2px solid #007bff; }
[data-state="loading"] { opacity: 0.5; }组合选择器
/* 后代选择器 */
.container p { margin-bottom: 1rem; }
/* 子选择器 */
.nav > .nav-item { padding: 10px; }
/* 相邻兄弟:段落紧跟标题时做引言样式 */
h1 + p { font-size: 1.2rem; font-weight: 600; }
/* 通用兄弟 */
h1 ~ p { color: #666; }
/* 分组 */
h1, h2, h3 { font-weight: 700; margin-bottom: 0.5rem; }伪类选择器
/* 链接伪类(顺序建议 LVHA) */
a:link { color: #007bff; }
a:visited { color: #6c757d; }
a:hover { color: #0056b3; text-decoration: underline; }
a:active { color: #004085; }
/* 表单伪类 */
input:focus { outline: 2px solid #007bff; outline-offset: 2px; }
input:focus-visible { outline: 2px solid #0056b3; }
input:checked { accent-color: #007bff; }
input:disabled { opacity: 0.5; cursor: not-allowed; }
input:invalid { border-color: #dc3545; }
/* focus-within:输入框聚焦时高亮整个表单组 */
.form-group:focus-within { background: #f0f8ff; }
/* 结构伪类 */
li:first-child { font-weight: 700; }
li:last-child { margin-bottom: 0; }
li:nth-child(2n) { background-color: #f5f5f5; } /* 斑马纹 */
li:nth-child(3n+1) { color: #007bff; } /* 每 3 个的第 1 个 */
li:only-child { list-style: none; }
p:empty { display: none; }
/* 逻辑伪类 */
.nav-item:not(.active) { opacity: 0.7; }
:is(h1, h2, h3) { font-weight: 700; } /* 提优先级 */
:where(h1, h2, h3) { margin: 0; } /* 优先级 0,易覆盖 */
/* :has() 父/关系选择器 */
article:has(img) { padding: 20px; } /* 含图片的文章 */
.form-group:has(input:invalid) label { color: #dc3545; } /* 校验失败标红 label */
figure:has(figcaption) { border: 1px solid #eee; }伪元素选择器
/* ::before / ::after */
.card::before {
content: '';
display: block;
width: 100%;
height: 4px;
background-color: #007bff;
}
.button::after { content: ' →'; margin-left: 5px; }
/* 用 ::before 做角标 */
.badge-new { position: relative; }
.badge-new::before {
content: 'NEW';
position: absolute;
top: -8px;
right: -8px;
font-size: 10px;
padding: 2px 6px;
background: #dc3545;
color: #fff;
border-radius: 10px;
}
/* 首字下沉 */
.article p::first-letter {
font-size: 2rem;
font-weight: 700;
float: left;
margin-right: 10px;
}
.article p::first-line { font-weight: 600; color: #333; }
/* 选中文本样式 */
::selection { background-color: #007bff; color: white; }
/* 占位符 */
input::placeholder { color: #999; font-style: italic; }
/* 列表标记 */
li::marker { color: #007bff; font-weight: 700; }优先级示例
/* 优先级:100(ID) */
#header { color: red; }
/* 优先级:20(两个类) */
.nav .item { color: blue; }
/* 优先级:112(一个 ID + 一个类 + 两个元素) */
div#nav .item a { color: green; }
/* 相同优先级:后定义者生效 */
.button { background-color: #007bff; }
.button { background-color: #0056b3; } /* 最终是这个 */
/* !important 覆盖普通声明(应尽量避免) */
.button { background-color: #dc3545 !important; }
/* :where() 优先级为 0,方便被主题覆盖 */
:where(.reset) ul { margin: 0; padding: 0; }级联层 @layer 控制覆盖顺序
/* 无论选择器谁更"重",靠后声明的 layer 整体优先级更高 */
@layer reset, base, components, utilities;
@layer components {
.btn { background: #007bff; color: #fff; }
}
@layer utilities {
.bg-red { background: #dc3545; } /* utilities 在后,覆盖 components */
}真实案例
案例 1:BEM 化解优先级战争。 某项目早期用后代选择器(`.sidebar .list .item a`)写样式,改版时新样式老被旧规则压住,工程师不断加 `!important`,最终一个组件里有 30 多个 important。重构为 BEM(`.sidebar__link` 单类)后,几乎所有选择器优先级都是 10,覆盖只靠源码顺序,important 数量降到个位数。
案例 2:`:has()` 去掉一段 JS。 某表单原来用 JavaScript 监听输入、给父容器加/删 class 来标红。用 `.field:has(input:invalid)` 后,纯 CSS 就能实现校验态联动,删掉约 40 行 JS,且状态与 DOM 始终同步、无闪烁。
案例 3:`:nth-child` 做表格斑马纹。 一个上千行的数据表格,原本后端渲染时给每行交替加 class。改用 `tr:nth-child(even)` 后,模板更干净,行增删也不用重算奇偶。
数据与对比
| 选择器类型 | 优先级贡献 | 推荐度 | 说明 |
| --- | --- | --- | --- |
| 内联 style | 1000 | 低 | 难覆盖,仅动态计算值用 |
| ID #id | 100 | 低 | 优先级过高,尽量别用于样式 |
| 类 .class | 10 | 高 | 团队协作主力 |
| 属性 [attr] | 10 | 中 | 适合状态钩子如 data-* |
| 伪类 :hover | 10 | 高 | 状态样式必备 |
| 元素 div | 1 | 中 | 基础排版、重置用 |
| 伪元素 ::after | 1 | 高 | 装饰内容利器 |
| 通用 * | 0 | 中 | 仅用于重置 |
| :where() | 0 | 高 | 写易被覆盖的默认值 |
浏览器支持参考:`:is()`/`:where()` 主流浏览器全支持;`:has()` 自 2023 年起 Chrome 105+、Safari 15.4+、Firefox 121+ 支持,覆盖率约 90%+,关键功能建议提供降级。
常见坑
最佳实践
属性选择器深入
属性选择器常被低估,其实它是"根据元素自身携带的数据选样式"的利器,尤其配合 `data-*` 做状态机非常干净。
/* 完整对照:不同匹配模式 */
[type] { } /* 有 type 属性 */
[type="checkbox"] { } /* 精确等于 */
[class~="btn"] { } /* 空格分隔列表里含 btn(等价于 .btn) */
[lang|="zh"] { } /* 等于 zh 或以 zh- 开头,如 zh-CN */
[href^="mailto:"] { } /* 以 mailto: 开头 */
[href$=".zip"] { } /* 以 .zip 结尾 */
[href*="github"] { } /* 含 github */
[data-size="lg" i] { } /* i:忽略大小写,匹配 LG/Lg/lg */
/* 用 data-* 表达组件状态,样式与状态一一对应 */
.dialog[data-open="true"] { display: block; }
.dialog[data-open="false"] { display: none; }
.tab[aria-selected="true"] { border-bottom: 2px solid #007bff; }
.btn[aria-busy="true"] { pointer-events: none; opacity: 0.6; }
.row[data-status="error"] { background: #fff5f5; }
/* 给外链、下载、不同文件类型自动加图标 */
a[target="_blank"]::after { content: ' ↗'; font-size: 0.8em; }
a[download]::before { content: '⬇ '; }
a[href$=".pdf"]::after { content: ' (PDF)'; color: #dc3545; }:nth-child 表达式实战
`:nth-child` 的参数是一个 `An+B` 表达式,威力远超"隔行变色"。理解它能省掉大量给元素手动加 class 的 JS。
/* 基础 */
li:nth-child(odd) { } /* 1,3,5... 奇数 */
li:nth-child(even) { } /* 2,4,6... 偶数 */
li:nth-child(3) { } /* 第 3 个 */
/* An+B 表达式 */
li:nth-child(3n) { } /* 3,6,9... 每三个的最后一个 */
li:nth-child(3n+1) { } /* 1,4,7... 每三个的第一个(每行首列) */
li:nth-child(n+4) { } /* 第 4 个及以后("超过 3 个就折叠") */
li:nth-child(-n+3) { } /* 前 3 个("只显示前三") */
/* 组合出"区间":第 3 到第 6 个 */
li:nth-child(n+3):nth-child(-n+6) { background: #f0f8ff; }
/* 从后往前数 */
li:nth-last-child(2) { } /* 倒数第 2 个 */
li:nth-last-child(-n+2) { } /* 最后两个 */
/* 结合 :only-child / :first-of-type 做边界处理 */
.tag:first-child { margin-left: 0; }
.tag:last-child { margin-right: 0; }一个高频真实需求:"列表超过 5 条就把多余的隐藏并显示『展开』"。纯 CSS 即可:
.list:not(.expanded) > li:nth-child(n+6) { display: none; }
.list.expanded > li { display: list-item; }选择器性能
浏览器从右往左匹配选择器。最右边那个(关键选择器 key selector)决定了初始候选集大小,因此它越具体越快。
/* 慢:关键选择器是标签 a,浏览器要先找页面上所有 a,再往上回溯 */
header nav ul li a { }
/* 快:关键选择器是一个类,候选集立刻缩小 */
.nav-link { }性能参考数据(量级示意,实际取决于引擎与 DOM 规模):
| 选择器写法 | 相对成本 | 说明 |
| --- | --- | --- |
| .single-class | 低 | 现代引擎哈希查找,接近 O(1) |
| tag(如 div) | 中 | 候选集大 |
| .a .b .c .d 深层后代 | 高 | 逐级回溯 |
| * 通用 | 中高 | 匹配所有元素 |
| :has() 复杂关系 | 中高 | 需评估关系,慎用大范围 |
结论:在绝大多数页面上选择器性能已不是瓶颈,但在超大 DOM(上万节点)或高频重排场景,扁平的单类选择器仍有意义。不要为了"性能"牺牲可读性,但也别写 6 层后代链。
命名方法论对比
选择器写法背后是一套组织哲学,主流三种:
| 方法 | 核心思想 | 优点 | 代价 |
| --- | --- | --- | --- |
| BEM | Block__Element--Modifier 单类 | 优先级扁平、语义清晰、可预测 | 类名长 |
| OOCSS | 结构与皮肤分离、可复用对象 | 复用高 | 需要设计抽象 |
| 原子化/Utility(Tailwind) | 一个类一条样式 | 几乎不写自定义 CSS、体积可控 | HTML 类名冗长、学习成本 |
BEM 示例(推荐团队默认):
/* Block */
.card { }
/* Element */
.card__title { }
.card__body { }
/* Modifier */
.card--featured { }
.card__title--large { }无障碍与选择器
选择器能配合 ARIA 属性表达可访问的状态,样式与语义同步,避免"看起来对但读屏软件读不出"的问题。
/* 焦点样式优先用 :focus-visible,鼠标点击不显示、键盘导航才显示 */
.btn:focus-visible { outline: 3px solid #0056b3; outline-offset: 2px; }
/* 用 ARIA 状态驱动样式,保证视觉与语义一致 */
[aria-current="page"] { font-weight: 700; }
[aria-expanded="true"] .caret { transform: rotate(180deg); }
[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
[aria-invalid="true"] { border-color: #dc3545; }
/* 视觉隐藏但保留给读屏(无障碍常用工具类) */
.visually-hidden {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}更多 :has() 关系选择器场景
`:has()` 落地后,很多以前必须靠 JS 的"根据内部状态改外部样式"都能纯 CSS 实现。
/* 卡片内有 <video> 时改用横向布局 */
.card:has(video) { display: grid; grid-template-columns: 200px 1fr; }
/* 选中某单选框时高亮它所在的整个 option 行 */
.option:has(input:checked) { border-color: #007bff; background: #f0f8ff; }
/* 表单任一字段无效时,禁用提交按钮所在的工具条 */
form:has(:invalid) .submit-bar { opacity: 0.5; }
/* 数量选择:列表为空时显示占位提示(配合 :empty) */
.list:has(li) + .empty-hint { display: none; }
/* 前一个兄弟被 hover 时,当前元素做出反应 */
.menu-item:has(+ .menu-item:hover) { opacity: 0.6; }更多真实案例
案例 4:下拉箭头随展开翻转。 折叠面板以前用 JS 切 class 控制箭头方向。改用 `[aria-expanded="true"] .caret { transform: rotate(180deg); }` 后,箭头状态和 ARIA 语义永远同步,读屏用户体验也正确,删掉了对应 DOM 操作代码。
案例 5:营销页"只显示前三、其余折叠"。 用 `li:nth-child(-n+3)` 与 `li:nth-child(n+4) { display:none }` 配合一个 checkbox(`:checked` 展开),实现零 JS 的"查看更多",首屏 DOM 更轻。
案例 6:打印样式。 用属性选择器给打印场景补充上下文:`@media print { a[href^="http"]::after { content: ' (' attr(href) ')'; } }`,打印出来的文章能显示链接真实地址,非常实用。
选择器匹配原理:为什么从右往左
理解浏览器如何匹配选择器,是写高效、可维护选择器的地基。
关键选择器(Key Selector):选择器最右边的那一段。浏览器先用它圈出候选元素集,再向左逐段验证祖先/兄弟关系。
/* 浏览器视角:先找所有 <a>(关键选择器),再逐个往上确认它在
.list 里、.list 又在 #nav 里 —— 候选集大,验证多 */
#nav .list a { }
/* 关键选择器是类,候选集立刻缩小到 .nav-link,几乎不用回溯 */
.nav-link { }为什么不是从左往右? 若从左往右,浏览器要先找到所有 `#nav`,再遍历其全部后代找 `a`——对每个元素都要向下遍历整棵子树,成本更高。从右往左能以"关键选择器"快速锁定小候选集,再向上验证(祖先链是有限且确定的)。
这决定了一条黄金法则:让关键选择器尽量具体(用类而非标签),别让它是宽泛的 `*` 或标签。
/* 慢:关键选择器 * */
.container * { box-sizing: border-box; }
/* 慢:关键选择器是标签,且链很长 */
body div.wrapper ul li span { }
/* 快:关键选择器是单类 */
.box-border { box-sizing: border-box; }属性选择器:状态机与数据驱动
属性选择器配合 `data-*` 和 ARIA,是"用元素自身数据驱动样式"的利器,比加 class 更语义化、更好维护。
/* 完整匹配模式对照 */
[disabled] { } /* 存在该属性 */
[type="email"] { } /* 精确等于 */
[class~="btn"] { } /* 空格列表含 btn,等价 .btn */
[lang|="en"] { } /* 等于 en 或 en- 开头(en-US、en-GB) */
[href^="https://"] { } /* 以...开头 */
[src$=".webp"] { } /* 以...结尾 */
[title*="警告"] { } /* 包含子串 */
[data-role="admin" i] { } /* i:忽略大小写 */
[data-role="admin" s] { } /* s:强制区分大小写 */
/* 用 data-* 做组件状态机:状态与样式一一对应 */
.accordion[data-state="open"] .panel { display: block; }
.accordion[data-state="closed"] .panel { display: none; }
.toast[data-type="success"] { border-color: #22c55e; }
.toast[data-type="error"] { border-color: #ef4444; }
.btn[data-loading] { pointer-events: none; opacity: 0.6; }
.step[data-status="done"]::before { content: "✓"; color: #22c55e; }
.step[data-status="active"]::before { content: "●"; color: #3b82f6; }
/* ARIA 属性驱动样式,保证视觉与语义同步 */
[aria-expanded="true"] .chevron { transform: rotate(180deg); }
[aria-selected="true"] { background: #eff6ff; }
[aria-current="page"] { font-weight: 700; }
[aria-busy="true"] { cursor: progress; }
[aria-hidden="true"] { display: none; }
/* 属性值提取到内容:打印时展开链接地址 */
@media print {
a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; }
}data 属性 vs class 表达状态:
| 维度 | class(is-active 等) | data 属性(data-state) |
|---|---|---|
| 互斥状态 | 需手动清除旧类 | 一个属性天然互斥 |
| 语义 | 一般 | 强(键值对) |
| JS 操作 | classList | dataset(更直观) |
| 多值状态 | 麻烦 | 天然支持 |
结构伪类完整体系
结构伪类按"位置"选元素,是免写 JS 分配 class 的利器。关键是分清 `child` 系与 `of-type` 系。
/* child 系:在所有兄弟中数位置 */
li:first-child { } /* 第一个子元素(必须是第一个兄弟) */
li:last-child { } /* 最后一个 */
li:only-child { } /* 唯一子元素 */
li:nth-child(3) { } /* 第 3 个兄弟 */
li:nth-last-child(2) { } /* 倒数第 2 个 */
/* of-type 系:在同标签兄弟中数位置 */
p:first-of-type { } /* 第一个 <p>(前面可以有别的标签) */
p:last-of-type { } /* 最后一个 <p> */
p:only-of-type { } /* 同级中唯一的 <p> */
p:nth-of-type(2) { } /* 第 2 个 <p> */
p:nth-last-of-type(1) { } /* 倒数第 1 个 <p> */
/* 其他结构伪类 */
:empty { } /* 无子节点(含文本)的元素 */
:root { } /* 文档根,通常是 <html>,特异性等于一个类 */child 与 of-type 的关键差异:
<div>
<h2>标题</h2> <!-- 第 1 个子元素,但是第 1 个 h2 -->
<p>段落一</p> <!-- 第 2 个子元素,但是第 1 个 p -->
<p>段落二</p> <!-- 第 3 个子元素,第 2 个 p -->
</div>p:first-child { } /* 不命中!第一个子元素是 h2,不是 p */
p:first-of-type { } /* 命中"段落一":它是第一个 p */这是最经典的踩坑点——想选"第一个段落"却用了 `:first-child`,因为前面有个标题而失效,应该用 `:first-of-type`。
:nth-child(An+B) 表达式大全
`nth-child` 的 `An+B` 是一门小语言,掌握它能替代大量 JS。
/* 基础 */
:nth-child(odd) /* 奇数 1,3,5... */
:nth-child(even) /* 偶数 2,4,6... */
:nth-child(3) /* 恰好第 3 个 */
/* An 步长 */
:nth-child(3n) /* 3,6,9...(每 3 个的最后一个) */
:nth-child(4n) /* 4,8,12... */
/* An+B 偏移 */
:nth-child(3n+1) /* 1,4,7...(每 3 个的第一个 = 网格每行首列) */
:nth-child(2n+1) /* 等价 odd */
:nth-child(5n-1) /* 4,9,14... */
/* 边界选择 */
:nth-child(n+4) /* 第 4 个及以后("超过 3 个的部分") */
:nth-child(-n+3) /* 前 3 个 */
:nth-child(n+2):nth-child(-n+5) /* 交集:第 2 到第 5 个 */
/* of S 语法:先过滤再计数 */
:nth-child(even of .visible) /* 只在 .visible 里数偶数 */
:nth-child(-n+3 of li.tag) /* 前 3 个 li.tag */实战需求集:
/* 需求:网格每行 3 列,给每行首列去掉左边距 */
.grid-item:nth-child(3n+1) { margin-left: 0; }
/* 需求:列表超过 5 条折叠,配合 checkbox 展开 */
.list:not(:has(.toggle:checked)) > li:nth-child(n+6) { display: none; }
/* 需求:前 3 名奖牌配色 */
.rank:nth-child(1) { color: gold; }
.rank:nth-child(2) { color: silver; }
.rank:nth-child(3) { color: #cd7f32; }
/* 需求:斑马纹只算可见行(隐藏行不打乱) */
tr:nth-child(even of :not([hidden])) { background: #f8f9fa; }:not() 深入
`:not()` 现代已支持复杂选择器列表,但要警惕它对特异性的影响。
/* 单参数 */
.btn:not(.disabled) { cursor: pointer; }
/* 选择器列表:任一匹配即排除 */
input:not([type="hidden"], [disabled], [readonly]) {
border: 1px solid #ddd;
}
/* 组合链 */
li:not(:first-child):not(:last-child) { } /* 中间的项 */
li:not(:first-child) { border-top: 1px solid #eee; } /* 除首项外都加上边框 */
/* 与其他伪类组合 */
.card:not(:has(img)) { padding: 12px; } /* 无图卡片紧凑 */
a:not([href]) { color: inherit; cursor: default; } /* 无 href 的锚点 */特异性陷阱:`:not()` 本身不加特异性,但它计入括号内特异性最高的那个。
:not(.a) /* 特异性 = 一个类(0,1,0) */
:not(#id) /* 特异性 = 一个 ID(1,0,0)—— 意外拔高! */
:not(.a, #id) /* 取列表最高 = ID(1,0,0) */所以 `div:not(#foo)` 的特异性会因为里面的 ID 而飙升,覆盖时可能出乎意料。需要"零特异性排除"时可套 `:where()`:
/* :where() 内部恒为 0,用它包住排除条件不拔高特异性 */
a:where(:not(.btn)) { text-decoration: underline; }:is() 与 :where():特异性的两把钥匙
二者语法相同、行为相同,唯一区别是特异性计算:`:is()` 取参数中最高,`:where()` 恒为 0。
/* :is() 简化并列,特异性 = 参数中最高者 */
:is(h1, h2, h3) a { color: var(--color-primary); }
/* 等价于展开三条,但特异性按 h_ + a 计算 */
/* :where() 简化并列,特异性 = 0,极易被覆盖 */
:where(h1, h2, h3) { margin-block: 0; }
/* 适合写"默认值/重置",业务样式无需提高特异性就能盖过 */
/* 对比:同样的分组,特异性天差地别 */
:is(.a, .b, #c) .target { } /* 特异性含 ID = (1,0,1) */
:where(.a, .b, #c) .target { } /* 特异性 = (0,0,1),忽略内部 */实用场景:库/框架用 `:where()` 提供默认样式,让业务方用最普通的类就能覆盖,避免"改个默认值还要提高特异性打架"。
/* 一个可被轻松覆盖的重置层 */
:where(ul, ol) { margin: 0; padding: 0; list-style: none; }
:where(a) { color: inherit; text-decoration: none; }
:where(button) { font: inherit; cursor: pointer; }
/* 业务侧:.nav a { color: blue; } 一个类就能盖过上面的 :where(a) */表单伪类状态机
表单是伪类最密集的地方,用好它们能做出无需 JS 的实时校验反馈。
/* 校验状态 */
input:valid { } /* 满足约束 */
input:invalid { } /* 不满足(但页面一加载空必填就红,体验差) */
input:user-valid { } /* 用户交互后才判定为有效(推荐) */
input:user-invalid { } /* 用户交互后才判定为无效(推荐) */
input:required { } /* 必填 */
input:optional { } /* 选填 */
input:in-range { } /* 数值在 min/max 内 */
input:out-of-range { } /* 超范围 */
/* 交互与内容状态 */
input:placeholder-shown{ } /* 正显示占位符(即为空) */
input:autofill { } /* 浏览器自动填充 */
input:read-only { } /* 只读 */
input:checked { } /* 勾选 */
input:indeterminate { } /* 半选(如全选框的中间态) */
input:default { } /* 默认选中项 */
/* 组合出浮动标签:占位符隐藏(有内容)或聚焦时,label 上浮 */
.field { position: relative; }
.field label {
position: absolute; left: 12px; top: 12px;
transition: all 0.15s; pointer-events: none;
}
.field input:focus + label,
.field input:not(:placeholder-shown) + label {
top: -8px; font-size: 0.75rem; color: var(--color-primary);
}
/* 用 :user-invalid 做友好校验:用户动过才标红 */
input:user-invalid {
border-color: #dc3545;
}
input:user-invalid + .error-hint { display: block; }
/* 自定义复选框(用 :checked 驱动) */
.checkbox input:checked + .box { background: var(--color-primary); }
.checkbox input:checked + .box::after { content: "✓"; color: #fff; }伪元素完整清单
伪元素创建"文档树里不存在的部分",用双冒号 `::`。除了常见的 before/after,还有一批实用的。
/* 生成内容 */
.tag::before { content: "#"; opacity: 0.5; }
.tag::after { content: ""; }
/* 首字下沉与首行 */
.article p::first-letter { font-size: 3rem; float: left; line-height: 1; }
.article p::first-line { font-variant: small-caps; }
/* 选中文本高亮 */
::selection { background: var(--color-primary); color: #fff; }
/* 占位符 */
input::placeholder { color: #9ca3af; }
/* 列表标记 */
li::marker { color: var(--color-primary); font-weight: 700; }
/* 文件上传按钮 */
input[type="file"]::file-selector-button {
padding: 6px 12px; border-radius: 6px;
background: var(--color-primary); color: #fff; border: none;
}
/* 对话框/弹窗遮罩 */
dialog::backdrop { background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(2px); }
/* 详情组件展开内容(新) */
details::details-content { transition: opacity 0.3s; }
/* 拼写/语法错误(新) */
::spelling-error { text-decoration: red wavy underline; }
::grammar-error { text-decoration: green wavy underline; }
/* 高亮定位:URL 里 #:~:text= 命中的文本 */
::target-text { background: #fef08a; }伪元素用途一览:
| 伪元素 | 用途 |
|---|---|
| ::before / ::after | 装饰、图标、角标、清浮动 |
| ::first-letter / ::first-line | 排版首字/首行 |
| ::selection | 自定义选中高亮 |
| ::placeholder | 占位符样式 |
| ::marker | 列表符号/序号 |
| ::backdrop | 全屏/弹窗遮罩 |
| ::file-selector-button | 文件上传按钮 |
| ::target-text | 文本片段定位高亮 |
特异性计算详解与练习
特异性是三元组 (ID 数, 类/属性/伪类 数, 元素/伪元素 数),逐位比较,前位大者胜,与源码顺序无关(顺序只在完全相等时才裁决)。
/* 逐个拆解 */
* /* (0,0,0) */
li /* (0,0,1) */
li::before /* (0,0,2) 元素 + 伪元素 */
.list /* (0,1,0) */
.list li /* (0,1,1) */
.list li.active /* (0,2,1) */
a[href] /* (0,1,1) 元素 + 属性 */
#nav /* (1,0,0) */
#nav .list li a /* (1,1,2) */
:is(#nav, .side) a /* (1,0,1) :is 取最高 = ID */
:where(#nav) a /* (0,0,1) :where 恒 0 */
:not(.a, #b) /* (1,0,0) :not 取内部最高 = ID */练习:谁生效?
/* A */ #main .card .title { color: red; } /* (1,2,0) */
/* B */ .card h2.title.big { color: blue; } /* (0,3,1) */
/* 答案:A 生效。第一位 1 > 0,直接判定,后面的位不用比。
即使 B 有更多类,也敌不过一个 ID。 *//* C */ .btn.primary { background: green; } /* (0,2,0) */
/* D */ .btn[data-variant] { background: teal; } /* (0,2,0) */
/* 答案:C 与 D 特异性相等 (0,2,0),此时看源码顺序,后写的 D 生效。 */这正是架构方法论的动机:让选择器尽量保持在 (0,1,0) 这一层(单类),覆盖只靠源码顺序或 @layer,就永远不会陷入"要盖过对方只能加 ID 或 !important"的军备竞赛。
@layer 与选择器优先级的关系
级联层把"覆盖顺序"从特异性比拼升级为显式层序,是选择器优先级治理的终极方案。
/* 声明层顺序:越靠后优先级越高,与特异性无关 */
@layer reset, base, components, utilities;
@layer components {
/* 即使这里用了高特异性 */
#app .card .title.big { color: black; } /* (1,3,0) */
}
@layer utilities {
/* 这个低特异性的类照样赢,因为 utilities 层更靠后 */
.text-red { color: red; } /* (0,1,0) */
}
/* 结果:.text-red 生效。层序 > 特异性。 */优先级裁决的完整顺序(从高到低):
选择器与无障碍
选择器配合 ARIA 与可访问性伪类,让样式与语义同步,兼顾好看与可用。
/* 键盘聚焦才显示焦点环,鼠标点击不显示 */
:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
/* 但绝不要 :focus { outline: none } 一刀切,会害了键盘用户 */
/* 焦点在容器内任意后代时高亮容器 */
.card:focus-within { box-shadow: 0 0 0 2px var(--color-focus); }
/* ARIA 驱动状态样式 */
[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
[aria-invalid="true"] { border-color: #dc3545; }
[aria-pressed="true"] { background: var(--color-primary); color: #fff; }
/* 尊重减少动效偏好 */
@media (prefers-reduced-motion: reduce) {
*, ::before, ::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* 视觉隐藏但保留给读屏 */
.sr-only {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}真实案例集(续)
案例 7:纯 CSS 标签页(Tabs)。 用 radio + `:checked` + 兄弟选择器实现无 JS 标签切换。
.tabs input[type="radio"] { display: none; }
.tabs input:nth-of-type(1):checked ~ .panels .panel:nth-child(1),
.tabs input:nth-of-type(2):checked ~ .panels .panel:nth-child(2) {
display: block;
}
.tabs .panel { display: none; }案例 8:树形菜单展开箭头。 用 `details` + `[open]` 伪类,箭头随展开翻转,零 JS。
details > summary { list-style: none; cursor: pointer; }
details > summary::before { content: "▶"; display: inline-block; transition: 0.2s; }
details[open] > summary::before { transform: rotate(90deg); }案例 9:表格列 hover 高亮(配合 :has)。 鼠标在某单元格时,用 `:has()` 反选高亮整列的思路(结合 nth-child 定位)。
/* 悬停某行时整行加深 */
tbody tr:hover { background: #f0f8ff; }
/* 表格容器在有选中行时显示批量操作条 */
.table-wrap:has(tbody input:checked) .bulk-bar { display: flex; }案例 10:根据内容语言自动排版。 用 `[lang|="ar"]` 给阿拉伯语内容切换方向与字体。
[lang|="ar"] { direction: rtl; font-family: "Noto Naskh Arabic", serif; }
[lang|="zh"] { font-family: "Noto Sans SC", sans-serif; letter-spacing: 0.02em; }选择器性能实测与建议
现代引擎(Blink/WebKit/Gecko)用 Bloom filter 和快速拒绝优化,绝大多数选择器已不是瓶颈。但在超大 DOM(上万节点)或高频 DOM 变更下仍需注意。
| 写法 | 相对成本 | 建议 |
|---|---|---|
| .single-class | 极低 | 首选 |
| tag(div、span) | 低 | 可用 |
| [attr] / [data-x] | 低 | 状态钩子首选 |
| .a .b .c 深层后代 | 中高 | 控制在 3 层内 |
| * 通用 | 中 | 仅重置用 |
| :has() 宽泛关系 | 中高 | 收窄作用域 |
| :not() 复杂列表 | 中 | 适度使用 |
实用结论:不要为微不足道的选择器性能牺牲可读性;但也别写 6 层后代链或全局宽泛 `:has()`。扁平单类既快又好维护,是双赢选择。
组合器深入:四种关系的实战
组合器(combinator)描述元素间的关系,用对能极大减少无谓的 class。
/* 后代(空格):任意层级后代 */
.article a { color: var(--color-primary); }
/* 子(>):仅直接子级,不穿透更深 */
.menu > li { border-bottom: 1px solid #eee; }
/* 只给一级菜单项加线,子菜单的 li 不受影响 */
/* 相邻兄弟(+):紧接其后的那一个 */
h2 + p { margin-top: 0; } /* 标题后第一段贴紧标题 */
.form-error + input { border-color: red; }
/* 通用兄弟(~):其后所有同级兄弟 */
h2 ~ p { color: #555; } /* 标题之后的所有段落 */
input:checked ~ label { font-weight: 700; }"猫头鹰"选择器(lobotomized owl):` + ` 给"除第一个外的所有元素"加间距,是经典的流式布局技巧。
/* 给垂直排列的子元素之间统一加间距,首元素不加 */
.flow > * + * { margin-block-start: 1rem; }
/* 相比给每个元素加 margin-bottom 再清最后一个,这个更优雅 */相邻兄弟做"上一个/下一个"联动:
/* 手风琴:展开项的下一个标题拉开距离 */
.item[data-open] + .item { margin-top: 8px; }
/* 表单:某选项选中后显示紧邻的补充输入 */
input[value="other"]:checked ~ .other-input { display: block; }更多状态与媒体伪类
除了常见伪类,还有一批"元素处于某种特殊状态"的伪类,用于弹窗、全屏、目标定位等。
/* :target —— URL 锚点命中的元素(纯 CSS 路由/弹窗) */
#modal:target { display: block; }
.section:target { animation: highlight 1s; }
/* :modal —— 通过 showModal() 打开的 dialog */
dialog:modal { max-width: 500px; }
/* :popover-open —— 打开状态的 popover */
[popover]:popover-open { animation: pop-in 0.2s; }
/* :fullscreen —— 全屏元素 */
video:fullscreen { object-fit: contain; }
/* :defined —— 已注册的自定义元素(避免 FOUC) */
my-widget:not(:defined) { visibility: hidden; }
/* :dir() —— 按书写方向选择 */
:dir(rtl) .arrow { transform: scaleX(-1); }
/* :lang() —— 按语言选择(比属性选择器更懂继承) */
:lang(zh) { line-height: 1.8; }
:lang(ja) q { quotes: "「" "」"; }
/* :playing / :paused —— 媒体播放状态(新) */
video:paused { filter: grayscale(0.3); }选择器在 JavaScript 中的复用
CSS 选择器不只用于样式,JS 的 DOM API 也用同一套语法,理解它们能少写很多手动遍历。
// querySelector / querySelectorAll —— 同 CSS 选择器语法
const firstBtn = document.querySelector('.toolbar > .btn:first-child');
const invalids = document.querySelectorAll('input:invalid');
// matches —— 判断元素是否匹配某选择器(事件委托常用)
document.addEventListener('click', (e) => {
if (e.target.matches('.btn, .btn *')) {
handleButtonClick(e);
}
});
// closest —— 向上找最近的匹配祖先(比手写 parentNode 循环干净)
const row = e.target.closest('tr[data-id]');
const id = row?.dataset.id;
// 属性/结构选择器在 JS 里同样有效
const openPanels = document.querySelectorAll('[data-state="open"]');
const evenRows = document.querySelectorAll('tbody tr:nth-child(even)');事件委托 + matches 的价值:不用给每个按钮单独绑事件,只在父级绑一个,用 `matches` 判断来源,动态增删的元素也自动生效。
CSS 嵌套中的选择器
原生 CSS 嵌套让选择器组织更贴近结构,但要注意 `&` 的用法。
.card {
padding: 16px;
/* & 拼接伪类 */
&:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
/* & 引用父级,写"父级在后"的关系 */
.theme-dark & { background: #1e293b; }
/* 嵌套后代 */
& .title { font-weight: 700; }
/* 嵌套 :has */
&:has(img) { padding-block: 24px; }
/* 嵌套媒体查询 */
@media (min-width: 768px) { padding: 24px; }
/* 嵌套 :is 列表 */
& :is(h1, h2, h3) { margin: 0; }
}注意:原生嵌套不支持 Sass 的 `&--modifier` 字符串拼接。BEM 修饰符要写全类名。
选择器调试技巧
/* 技巧 1:outline 大法 —— 给可疑选择器加醒目轮廓,确认命中范围 */
.debug-target { outline: 2px solid magenta !important; }
/* 技巧 2:给所有元素描边,排查布局(不占空间,不用 border) */
* { outline: 1px solid rgba(255,0,0,0.2); }
/* 技巧 3:用属性存在选择器找出漏标的元素 */
img:not([alt]) { outline: 3px solid red; } /* 缺 alt 的图 */
a:not([href]) { outline: 3px solid orange; } /* 无 href 的链接 */
[style] { outline: 2px dashed purple; } /* 有内联样式的(应消除) */// 在控制台快速统计某选择器命中数量
console.log(document.querySelectorAll('.card:has(img)').length);
// 找出所有用了 !important 的元素(结合 DevTools)
// 或用 getComputedStyle 验证最终生效值
const el = document.querySelector('.btn');
console.log(getComputedStyle(el).backgroundColor);常见面试题
Q1:`div p` 和 `div > p` 的区别?
`div p` 选 div 内任意层级的 p;`div > p` 只选 div 的直接子 p。
Q2:`.a.b` 和 `.a .b` 的区别?
`.a.b`(无空格)选同时拥有 a 和 b 两个类的元素;`.a .b`(有空格)选 `.a` 后代里的 `.b`。这是初学者最常混的。
<div class="a b">同时有 a 和 b —— .a.b 命中</div>
<div class="a"><div class="b">.a 里的 .b —— .a .b 命中</div></div>Q3:`:nth-child(2)` 和 `:nth-of-type(2)` 区别?
`:nth-child(2)` 要求"是父级的第 2 个子元素且匹配前面的类型";`:nth-of-type(2)` 是"同类型元素中的第 2 个",不管中间夹了别的标签。
Q4:如何选中"没有子元素的元素"?
`:empty`(连文本节点都没有)。注意有空格或注释也算非空。
Q5:`:where(.a) .b` 的特异性是多少?
(0,0,1)。`:where()` 内部特异性归零,只剩 `.b` 这个类……不对,`.b` 是类,应为 (0,1,0)?——准确说 `:where(.a)` 贡献 0,`.b` 贡献 (0,1,0),合计 (0,1,0)。这道题考的就是 `:where()` 抹掉内部特异性。
Q6:为什么 `a:hover` 要写在 `a:visited` 后面?
LVHA 顺序(link-visited-hover-active)。因为它们特异性相同,靠源码顺序裁决;hover 写在 visited 前会被 visited 覆盖,导致悬停变色失效。
:has() 场景大全(进阶)
`:has()` 是"关系选择器",能表达过去 CSS 无法表达的条件,这里汇总高频模式。
/* 1. 根据后代存在性 */
.card:has(img) { } /* 含图 */
.card:not(:has(img)) { } /* 无图 */
figure:has(figcaption) { } /* 有说明文字 */
/* 2. 根据后代状态 */
label:has(input:checked) { } /* 关联输入被选中 */
form:has(:invalid) { } /* 表单有无效项 */
tr:has(:checked) { } /* 行内有勾选 */
/* 3. 根据数量(配合 nth) */
.grid:has(> :nth-child(4)) { } /* 至少 4 个子项 */
.list:has(> :only-child) { } /* 恰好 1 个 */
/* 4. 兄弟关系 */
h2:has(+ p) { } /* 后面紧跟段落的标题 */
.item:has(~ .item:hover) { } /* 后面有兄弟被 hover */
/* 5. 全局态(挂在 body/html 上) */
body:has(dialog[open]) { overflow: hidden; }
html:has(#dark-toggle:checked){ color-scheme: dark; }
/* 6. 组合复杂条件 */
.product:has(.badge--sale):has(img) { } /* 既打折又有图 */降级策略:`:has()` 覆盖率虽已约 90%+,关键功能仍建议提供 JS 兜底。
// 特性检测,不支持时用 JS 加 class 模拟
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('.card').forEach((c) => {
if (c.querySelector('img')) c.classList.add('has-image');
});
}真实案例:无 JS 的交互组件集
案例 11:纯 CSS 模态框。 用 `:target` 实现点击打开、点击遮罩关闭。
.modal { display: none; }
.modal:target { display: flex; } /* URL #modal 时显示 */
.modal:target .backdrop { /* 点击链接指向 # 即可关闭 */ }案例 12:图片对比滑块。 用 `:hover` + 兄弟选择器,鼠标移动区分前后图(简化版)。
案例 13:表单进度指示。 用 `:has()` 检测各步骤填写状态,纯 CSS 点亮进度条。
.wizard:has(#step1:valid) .progress { --step: 1; }
.wizard:has(#step2:valid) .progress { --step: 2; }
.progress::before { width: calc(var(--step) * 33.3%); }案例 14:空状态自动切换。 列表为空时显示占位,有内容时隐藏占位,全靠 `:has`/`:empty`。
.list:empty + .empty-state { display: block; }
.list:not(:empty) + .empty-state { display: none; }选择器优先级速查与决策
面对"样式没生效",按这个顺序排查往往一击即中:
| 想提高优先级 | 想降低优先级 |
|---|---|
| 加一个类(`.btn.btn`) | 用 `:where()` 包裹 |
| 用 `:is()` 引入高特异性项 | 放进更低的 @layer |
| (不推荐)加 ID / !important | 减少嵌套、去掉 ID |
综合实战:用选择器搭一个完整卡片组件
把前面的知识串起来,看一个真实卡片组件如何只用扁平、语义化的选择器完成全部交互态。
/* 结构:单类为主,特异性统一在 (0,1,0) */
.card {
--pad: 16px;
display: flex;
flex-direction: column;
gap: 12px;
padding: var(--pad);
border: 1px solid var(--color-border);
border-radius: 8px;
transition: box-shadow 0.2s, transform 0.2s;
}
/* 交互态:伪类而非 JS 加类 */
.card:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.1); transform: translateY(-2px); }
.card:focus-within { outline: 2px solid var(--color-primary); }
/* 条件布局::has 根据内容自适应 */
.card:has(.card__media) { padding-top: 0; }
.card:not(:has(.card__media)) { --pad: 20px; }
/* 结构伪类:首尾元素特殊处理 */
.card__body > p:first-of-type { margin-top: 0; }
.card__body > p:last-child { margin-bottom: 0; }
/* 状态:data 属性驱动 */
.card[data-featured] { border-color: var(--color-primary); }
.card[data-featured] .card__title::after { content: " ★"; color: gold; }
.card[data-loading] { opacity: 0.6; pointer-events: none; }
/* 数量自适应:标签超过 3 个时折叠多余的 */
.card__tags:has(> :nth-child(4)) > :nth-child(n+4) { display: none; }
.card__tags:has(> :nth-child(4))::after { content: "…"; }
/* 变体:BEM modifier */
.card--compact { --pad: 8px; gap: 6px; }
.card--horizontal { flex-direction: row; }这个组件的选择器特征:几乎全是单类 (0,1,0),交互靠伪类、状态靠 data 属性、条件布局靠 `:has`、边界靠结构伪类——没有一个 ID、没有一个 !important、没有深层后代链。这就是"优先级扁平、可预测"的样板。
选择器团队规范建议
大型项目里,选择器写法应有明确约定并用工具强制。
| 规范项 | 建议 |
|---|---|
| 主力选择器 | 类选择器(BEM 命名) |
| 最大嵌套深度 | ≤ 3 层 |
| ID 用于样式 | 禁止(可用于 JS 钩子/锚点) |
| !important | 禁止(工具类除外,且走 @layer) |
| 状态表达 | 伪类 + data- / aria-,不靠 JS 加类改样式 |
| 默认/重置样式 | 用 `:where()` 保持 0 特异性 |
| 覆盖顺序 | 用 @layer 显式编排 |
// stylelint 强制:最大特异性、禁 ID、限嵌套深度、BEM 命名
module.exports = {
rules: {
'selector-max-specificity': '0,3,0',
'selector-max-id': 0,
'selector-max-compound-selectors': 3,
'selector-class-pattern':
'^[a-z]([a-z0-9-]+)?(__([a-z0-9-]+))?(--([a-z0-9-]+))?$',
'no-descending-specificity': true, // 防止低特异性写在高特异性后面失效
},
};更多特异性计算练习
多做几道计算题,把规则内化成直觉。
/* 逐一计算特异性 */
ul li a /* (0,0,3) 三个元素 */
.nav > li > a /* (0,1,2) 一个类 + 两个元素 */
a.active:hover /* (0,2,1) 两个类(含伪类) + 一个元素 */
input[type="text"]:focus /* (0,2,1) 属性 + 伪类 + 元素 */
#main ul.list li:first-child /* (1,2,2) 一 ID + 两类 + 两元素 */
:is(.a, .b) :where(#x) .c /* (0,2,0) :is 取 .a/.b 一个类, :where 归 0, .c 一个类 */
::before /* (0,0,1) 伪元素算元素 */
*, :where(*) /* (0,0,0) 通用与 :where 都是 0 */综合裁决题:
/* 页面上一个 <a class="link active" href="#"> 同时被下列命中 */
/* R1 */ a { color: black; } /* (0,0,1) */
/* R2 */ .link { color: gray; } /* (0,1,0) */
/* R3 */ .link.active { color: blue; } /* (0,2,0) */
/* R4 */ a.active:hover { color: red; } /* (0,2,1) 仅 hover 时 */
/* 静止时:R3 生效(蓝色),(0,2,0) 最高。
悬停时:R4 生效(红色),(0,2,1) > (0,2,0)。 */属性选择器的进阶妙用
/* 用 attr() 把属性值渲染成内容(提示、计数) */
[data-count]::after { content: " (" attr(data-count) ")"; }
[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute;
/* ...气泡样式 */
}
/* 根据外链类型自动加图标 */
a[href^="mailto:"]::before { content: "✉ "; }
a[href^="tel:"]::before { content: "☎ "; }
a[href$=".pdf"]::after { content: " [PDF]"; color: #dc3545; }
a[target="_blank"]::after { content: " ↗"; }
/* 表单必填标记 */
label:has(+ input:required)::after,
input:required + label::after { content: " *"; color: #dc3545; }
/* 多值属性匹配(class 是空格列表) */
[class~="urgent"] { border-left: 3px solid red; } /* 等价 .urgent */选择器与 CSS 变量的作用域联动
选择器决定了 CSS 变量在哪里"落地",二者配合能做出优雅的上下文样式。
/* 在选择器命中的作用域内重定义变量,子树整体响应 */
.theme-accent {
--color-primary: #7c3aed; /* 该子树内主色变紫 */
}
.theme-accent .btn { background: var(--color-primary); }
/* 用状态伪类切换变量值 */
.card { --lift: 0; transition: transform 0.2s; transform: translateY(var(--lift)); }
.card:hover { --lift: -4px; }
/* 用属性选择器切换整套令牌(多尺寸组件) */
.btn[data-size="sm"] { --btn-pad: 4px 8px; --btn-font: 12px; }
.btn[data-size="md"] { --btn-pad: 8px 16px; --btn-font: 14px; }
.btn[data-size="lg"] { --btn-pad: 12px 24px; --btn-font: 16px; }
.btn { padding: var(--btn-pad); font-size: var(--btn-font); }这个模式的价值:不用为每个尺寸写一套完整样式,只切换变量,组件主体样式复用。选择器负责"在什么条件下",变量负责"取什么值",职责清晰。
国际化中的选择器
多语言项目里,`:lang()`、`:dir()` 和 `[lang|=...]` 能优雅处理语言差异。
/* :lang() 懂继承:即使子元素没标 lang,也能从祖先继承判定 */
:lang(zh) { font-family: "Noto Sans SC", sans-serif; }
:lang(ja) { font-family: "Noto Sans JP", sans-serif; }
:lang(ar) { font-family: "Noto Naskh Arabic", serif; direction: rtl; }
/* :dir() 按书写方向翻转图标 */
:dir(rtl) .icon-arrow { transform: scaleX(-1); }
/* 不同语言的引号样式 */
:lang(zh) q { quotes: "“" "”"; }
:lang(fr) q { quotes: "« " " »"; }
/* 逻辑属性配合,天然适配 RTL(配合选择器命中) */
.card { padding-inline-start: 16px; } /* LTR 左内边距,RTL 自动右 */选择器常见误区快查
把最容易犯的错集中列出,写代码时对照自查。
/* 误区 1:想选第一段却用 :first-child(前面有标题就失效) */
p:first-child { } /* ✗ 常常不命中 */
p:first-of-type { } /* ✓ */
/* 误区 2:.a .b 与 .a.b 混淆 */
.btn .icon { } /* .btn 后代里的 .icon */
.btn.icon { } /* 同时有 .btn 和 .icon */
/* 误区 3::not() 里放 ID 意外拔高特异性 */
div:not(#x) { } /* 特异性含 ID = (1,0,1) */
/* 误区 4:忘记 content,伪元素不生成 */
.badge::after { } /* ✗ 无 content 不显示 */
.badge::after { content: ""; } /* ✓ */
/* 误区 5::hover 写在 :visited 前被覆盖(LVHA) */
/* 误区 6:用 * 做关键选择器,性能与可读性双输 */
.container * { } /* ✗ 宽泛 */
.container > .item { } /* ✓ 关键选择器更具体 */
/* 误区 7::empty 对含空格/换行的元素不生效(那算有文本节点) */
p:empty { display: none; } /* <p> </p> 里有空格就不命中 */
/* 误区 8:属性值大小写敏感,需要时加 i 标志 */
[data-x="ADMIN"] { } /* 精确匹配,区分大小写 */
[data-x="admin" i] { } /* 忽略大小写 */选择器学习路线小结
给不同阶段的开发者一个进阶清单:
| 阶段 | 应掌握 |
|---|---|
| 入门 | 元素/类/ID、后代/子、基础伪类(hover/focus) |
| 进阶 | 属性选择器、结构伪类、nth-child 表达式、特异性计算 |
| 熟练 | :is/:where/:not、@layer、data-*/aria 状态驱动 |
| 精通 | :has 关系选择器、of S 语法、性能与可维护性权衡 |
总结
| 场景 | 推荐选择器写法 |
| --- | --- |
| 组件通用样式 | 单类 BEM(.block__el--mod) |
| 状态切换 | 伪类 :hover/:checked 或 [data-state] |
| 根据后代反选父级 | :has() |
| 一次匹配多个选择器 | :is()(提优先级)/ :where()(0 优先级) |
| 斑马纹/隔行 | :nth-child(even/odd) |
| "同类型第 n 个" | :nth-of-type(n) |
| 过滤后再计数 | :nth-child(An+B of S) |
| 装饰性内容 | ::before / ::after + content |
| 表单实时校验 | :user-invalid / :valid + 兄弟选择器 |
| 焦点可访问 | :focus-visible / :focus-within |
| 排除某些元素 | :not()(注意特异性) |
| 按语言/方向 | :lang() / :dir() |
| 控制整体覆盖顺序 | @layer 级联层 |
选择器能力速记表:
| 我想…… | 用什么 |
| --- | --- |
| 选同时有两个类的 | `.a.b` |
| 选 a 里面的 b | `.a .b` |
| 选直接子级 | `>` |
| 选紧邻的下一个 | `+` |
| 选后面所有兄弟 | `~` |
| 给间隔加边距 | ` + ` |
| 根据内容改父样式 | `:has()` |
| 写不易覆盖不了的默认值 | `:where()` |
| 提高一组选择器优先级 | `:is()` |
一句话记忆:选择器越精准越强大,也越难被覆盖。好的样式架构追求的是"优先级尽量扁平、可预测",让覆盖靠源码顺序和分层,而不是靠越写越重的选择器和 `!important`。
从更高的视角看,选择器不只是"选中谁"的工具,它还是样式与结构、状态、语义之间的契约:用类表达组件、用伪类表达状态、用属性选择器表达数据、用 `:has()` 表达关系、用 `@layer` 表达优先级意图。当每一种意图都用最贴切的选择器表达时,样式表就会变得自解释、可预测、易维护——这才是掌握选择器的终极意义。