如果你已经写了好几年 CSS,可能会觉得”选择器”这种基础话题没什么好聊的——不就是 .class、#id、div > p 那一套吗?但这几年 CSS 选择器其实悄悄发生了一次大升级,很多以前只能靠 JavaScript 才能实现的效果,现在一行选择器就能搞定。
这篇文章不讲那些人人都会的基础知识,而是把 2026 年真正值得你用起来的选择器捋一遍,顺便告诉你什么时候能放心用、什么时候还得留个心眼。
一、基础选择器:三句话带过
这部分默认你已经会了,快速过一遍防止漏掉细节:
- 类型/类/ID 选择器:
div、.card、#header,优先级依次升高。 - 组合器:
A B(后代)、A > B(子元素)、A + B(紧邻兄弟)、A ~ B(普通兄弟)。 - 优先级计算:内联样式 > ID > 类/属性/伪类 > 标签/伪元素,同级别看谁写在后面。
真正拉开差距的,是下面这些”现代选择器”。
二、分组免责声明::is() 和 :where()
写多了 CSS 都遇到过这种重复代码:
header h1,
header h2,
footer h1,
footer h2 {
font-family: serif;
}
用 :is() 可以直接压缩成一行:
:is(header, footer) :is(h1, h2) {
font-family: serif;
}
:is() 和 :where() 功能几乎一样,唯一的区别是优先级计算方式不同:
:is()会取括号里”优先级最高的那个选择器”作为整体优先级。:where()无论里面写什么,优先级永远是 0。
这个区别很关键。如果你在写一套组件库或者设计系统,想让使用者能轻松用一个类名覆盖你的样式,那就该用 :where(),因为它不会占用优先级,后来者一个 .my-override 就能盖过去。
反过来,如果你希望这段样式有一定的”分量”,不容易被意外覆盖,就用 :is()。
它们还有个隐藏福利,叫”宽容选择器列表”(forgiving selector list)——括号里就算有一个浏览器不认识的选择器,整条规则也不会全部失效,只是那一部分不生效。
普通的逗号分组选择器可做不到这点,只要有一个写错或者浏览器不支持,整条规则直接作废。
三、真正的”父选择器”::has()
这是这几年 CSS 里公认最大的突破。CSS 选择器一直只能”往下看”或者”往旁边看”(子元素、兄弟元素),没法”往上看”选中父元素——直到 :has() 出现。
/* 一个 figure 里如果有 figcaption,就加下边距 */
figure:has(figcaption) {
margin-bottom: 2rem;
}
/* 输入框如果是必填且无效,让相邻的 label 变红 */
label:has(+ input:required:invalid) {
color: crimson;
}
/* 卡片里如果有图片,就用不同的排版 */
.card:has(img) {
display: grid;
grid-template-columns: 120px 1fr;
}
以前想实现”根据子元素状态改变父元素样式”,基本只能靠 JavaScript 监听事件、动态加类名。
现在这些场景都能用纯 CSS 声明式地写出来,不用担心时序问题,也不会有 hydration 不一致的坑。
兼容性提醒:2026 年这个选择器已经被认定为”广泛可用”(Baseline Widely Available),四大浏览器引擎全部支持多年,可以放心在生产环境使用,基本不需要额外做兼容处理。
唯一要注意的是性能——:has() 需要遍历整个子树去匹配,如果括号里的条件写得太宽泛,或者作用的容器子元素特别多,可能会拖慢选择器匹配速度。
经验法则是:让左边的”主体”元素范围尽量小,让括号里的条件尽量精确。
如果你的项目还要兼容非常老的浏览器,可以用特性检测包一层:
@supports selector(:has(a)) {
.card:has(img) {
display: grid;
}
}
注意这里用的是 selector() 语法,问的是”浏览器认不认识这个选择器写法”,跟检测某个 CSS 属性支持情况的写法不太一样。
四、:nth-child() 的进化版:of 语法
大家都用过 :nth-child(2n) 选偶数行、:nth-child(odd) 选奇数行,但很多人不知道它现在还支持一个 of 从句,可以先筛选出符合某个条件的元素,再按顺序数第几个。
/* 在所有"可见"的列表项里,选出第 2 个 */
li:nth-child(2 of :not(.hidden)) {
font-weight: bold;
}
这个写法在处理”有些元素被隐藏或被过滤掉”的列表时特别好用——以前你只能先数出所有元素的位置再排除,现在可以直接对”筛选后的子集”计数。
这个语法已经进入广泛可用阶段,主流浏览器都支持,可以放心使用。
五、原生 CSS 嵌套:不用预处理器也能写”套娃”
Sass、Less 这类预处理器最大的卖点之一就是嵌套写法,现在浏览器原生就支持了:
.card {
padding: 1rem;
h2 {
font-size: 1.25rem;
}
&:hover {
box-shadow: 0 2px 8px rgb(0 0 0 / 0.1);
}
&.card--featured {
border: 2px solid gold;
}
.dark & {
background: #1a1a1a;
}
}
几个使用要点:
- 简单的后代选择器(比如上面的
h2),&可以省略,浏览器会自动理解。 - 但如果你要把父选择器接在自己后面(比如
&:hover、&.active),或者父选择器不在开头位置(比如.dark &表示”当祖先有.dark类时”),&就必须写出来。 - 嵌套只是”语法糖”,最终会被浏览器展开成和手写扁平选择器完全一样的优先级,不会有优先级方面的意外。
- 嵌套规则里还能直接塞
@media、@container、@supports,把响应式逻辑和样式写在一起,不用再跳出去单独写一段媒体查询。
这个特性目前也已经是广泛可用状态。如果你的项目只是用 Sass 图个嵌套方便,现在其实可以考虑砍掉这层构建工具了——不过 Sass 里的变量运算、mixin、循环这些能力,原生 CSS 还是没有完全对应的替代品,该保留的还是要保留。
六、交互与可访问性相关的伪类
这几个不算全新,但很多人还是习惯性用错,顺带纠正一下:
:focus-visible:只在浏览器判断”用户是靠键盘/辅助设备聚焦”时生效,鼠标点击一般不会触发。用它做聚焦样式,能避免鼠标用户看到一圈莫名其妙的高亮框,同时不牺牲键盘用户的可访问性。:focus-within:只要元素内部有任何子元素获得了焦点,这个元素本身就会匹配。常用来做”表单组高亮”——用户点进组内任意一个输入框,整个表单组边框变色。
button:focus-visible {
outline: 2px solid #2563eb;
}
.form-group:focus-within {
border-color: #2563eb;
}
七、属性选择器的几个冷门但好用的写法
/* 属性值以某个字符串开头 */
a[href^="https://"] { color: green; }
/* 属性值以某个字符串结尾 */
img[src$=".webp"] { /* ... */ }
/* 属性值包含某个子串 */
[class*="col-"] { /* ... */ }
/* 忽略大小写匹配 */
[data-status="Active" i] { /* ... */ }
最后这个 i 标志很多人不知道,加上之后属性值匹配就不区分大小写了,处理后端传来的、大小写不统一的数据字段时很方便。
八、怎么在实际项目里做选择?
给几条实用建议,不用死记硬背:
- 能用
:has()解决的”根据内容改父样式”问题,别再写 JS 了。 减少一次事件监听,就少一个潜在的时序 bug。 - 写组件库/设计系统,优先用
:where()包裹你的基础样式,把优先级”预算”留给使用者。 - 原生嵌套 +
:is()/:where()组合使用,能大幅减少选择器重复,代码可读性会提升不少。 - 老项目要兼容旧浏览器时,用
@supports selector(...)做特性检测,而不是简单粗暴地判断浏览器版本号。 :has()别嵌套太深、条件别写太宽,大表格、长列表这种场景尤其要注意性能。
写在最后
CSS 选择器这几年的进化,核心思路就一个:把以前必须靠 JavaScript 才能表达的”关系”和”状态”,变成声明式的 CSS 语句。少一行 JS,就少一份心智负担,也少一个可能出错的地方。
这些选择器现在基本都已经进入了主流浏览器的”广泛可用”阶段,该用就用起来,没必要再等了。