写 CSS 时最常冒出的疑问不是「这个属性怎么用」,而是「我这条规则到底选中了谁」以及「为什么另一条规则把它盖掉了」。这两个问题的答案都指向同一套东西:CSS 选择器(selector)——浏览器用来从 DOM 里筛出元素的一套匹配模式;而 :has()、:is()、:where() 这类函数伪类,则是近年来最灵活的匹配能力。本文把匹配规则、伪类、优先级与层叠一次串起来讲明白,文末再辨析一个最容易被混为一谈的概念:filter 滤镜。

ℹ️
术语统一
本文统一使用规范术语选择器(selector),指决定哪些元素命中样式的匹配模式;:is()、:where()、:not()、:has() 这类函数伪类同样属于选择器体系。注意 filter / backdrop-filter 是滤镜属性(对已命中的元素做视觉处理),与选择器完全是两回事。本文主体讲选择器,最后一节讲滤镜。

一条 CSS 规则里,谁负责选中元素

规则的结构
selector(选择器)→ 筛出元素集合
↓
declaration block(声明块)→ 应用到这些元素
↓
cascade(层叠)→ 多条规则冲突时决定谁生效
css 一条规则
.card.featured > h2 {
  color: #0066ff;
}

上面这条规则里,.card.featured > h2 是选择器:先筛出同时带 card 和 featured 类的元素,再取其直接子级的 h2。选择器只负责「筛」,花括号里的声明才负责「改」。而「改了却没生效」,属于层叠问题,不是选择器没写对——这是两个独立阶段,分开排查效率最高。

选择器能力全景

MDN 把选择器归为几大类,记住这张地图,后面所有语法都只是往格子里填东西。

类别代表写法按什么筛
基础选择器* div .card #app标签名、class、id
组合器div p div > p h2 + p h2 ~ p元素在 DOM 树中的关系
属性选择器[type="text"] [href^="https"]属性是否存在、值是否匹配
伪类:hover :nth-child(2n) :checked状态、位置、结构
函数伪类:is() :where() :not() :has()逻辑组合 / 反向条件
伪元素::before ::first-line ::marker不存在于 HTML 中的局部

基础选择器与「复合」

基础选择器彼此可以不加空格直接连写成复合选择器,表示「同时满足」;用逗号分隔则是选择器列表,表示「任一满足」。

css 基础与复合
* { box-sizing: border-box; }        /* 通配:所有元素,特异性为 0 */
p  { line-height: 1.8; }             /* 类型选择器 */
.card { padding: 16px; }             /* 类选择器 */
#toc { position: sticky; }           /* ID 选择器 */

h1, h2, .title { font-weight: 700; } /* 选择器列表:并集 */
.card.featured { border-color: #0066ff; }  /* 复合:同时有两类,中间不能有空格 */
button.primary.large { min-width: 96px; }  /* 三个条件同时成立 */
⚠️ 注意:.card .featured(中间有空格)是「.card 内部的 .featured」,而 .card.featured(无空格)是「同一个元素上同时有两个类」。一个空格造成的语义差别,是新手最常见的选择器误用。

四种组合器:按关系筛

html 示例结构
<article class="post">
  <h2>标题</h2>
  <p>第一段</p>
  <section>
    <p>嵌套段落</p>
  </section>
  <p>最后一段</p>
</article>
写法名称上面结构中命中
.post p后代组合器(空格)3 个 p,含嵌套的那个
.post > p子组合器2 个直接子级 p
h2 + p相邻兄弟组合器「第一段」
h2 ~ p后续兄弟组合器「第一段」「最后一段」(同父、在其后,不要求紧邻)

读复杂选择器有个固定技巧:从右往左读。.post > ul li.active a:hover 先找处于 hover 的 a,再看它是不是在 li.active 里、在 ul 里、且 ul 是 .post 的直接子级。浏览器实际也是从右往左匹配的,这一点直接影响性能(后面会讲)。

属性选择器:不用加 class 也能筛

按属性的存在、精确值、前后缀、子串来筛,做表单、链接、国际化样式时特别省事。

语法含义
[attr]存在该属性
[attr=value]值完全等于 value
[attr~=value]值是空格分隔的词列表,其中一项等于 value
[attr|=value]值等于 value 或以 value- 开头(常用于语言子标签)
[attr^=value]值以 value 开头
[attr$=value]值以 value 结尾
[attr*=value]值包含 value 子串
[attr=value i] / s忽略 / 强制 ASCII 大小写敏感
css 属性选择器实战
input[required] { border-left: 3px solid #ee0a24; }
a[href^="https"]::after { content: " ↗"; }
a[href$=".pdf"]::before { content: "📄 "; }
img[alt=""] { outline: 2px dashed #f59e0b; }  /* 找出漏写 alt 的图 */
[lang|="zh"] { font-family: "PingFang SC", sans-serif; }
a[href*="5dphp" i] { color: #0066ff; }
💡 小技巧:在 HTML 中,class、id、data-* 以及 role / aria-* 的值匹配是区分大小写的;而 type 这类规范定义的枚举属性值默认不区分大小写。不确定时加 i 标志最省心。

伪类:按状态、位置与结构筛

伪类用单冒号,筛的是「DOM 树里没直接写出来的信息」:交互状态、表单状态、在兄弟中的位置、元素的空/满状态。

css 四类常用伪类
/* 用户行为 */
.btn:hover { background: #0066ff; }
.btn:focus-visible { outline: 2px solid #00d4ff; }  /* 键盘聚焦才显示 */
.btn:active { transform: translateY(1px); }

/* 表单状态 */
input:checked + label { font-weight: 700; }
input:disabled { opacity: .5; }
input:placeholder-shown { border-color: #c9c9c9; }
input:invalid { border-color: #ee0a24; }

/* 树结构 */
li:first-child { margin-top: 0; }
li:last-child { border-bottom: 0; }
p:empty { display: none; }         /* 连空白字符都不能有 */
.card:only-child { width: 100%; }

/* 按位置匹配 */
tr:nth-child(odd) { background: #f7f9fc; }
li:nth-child(-n+3) { color: #0066ff; }   /* 前三项 */

nth-child(an+b) 到底怎么算

元素下标从 1 开始,而公式里的 n 从 0 开始取非负整数。把 n=0,1,2… 代进去,得到的正整数就是命中位置。

写法命中位置
odd / 2n+11, 3, 5, …
even / 2n2, 4, 6, …
3仅第 3 个
n+7第 7 个及之后所有
-n+3前 3 个
3n+44, 7, 10, …
n+8 与 -n+15 连用第 8 到第 15 个

两个高频混淆点:其一,:nth-child() 数的是所有兄弟元素,只有「该位置上的元素恰好也满足选择器前部分」才算命中;:nth-of-type() 只数同标签兄弟。其二,Selectors Level 4 还支持 :nth-child(2 of .active) 这种 of <selector> 写法——先在符合 .active 的子集里编号,能优雅解决「隐藏行导致斑马纹错乱」的问题。

现代选择器四件套:is / where / not / has

这四个函数伪类才是真正意义上「按条件筛」的工具,也是现代 CSS 少写 JS 的关键。

css 四个函数伪类
/* :is() 把冗长的并集压成一行,参数取「最具体」的特异性 */
:is(section, article, aside, nav) h1 { font-size: 1.5rem; }

/* :where() 同样压缩写法,但特异性恒为 0,方便被覆盖 */
:where(.theme-dark, .theme-light) .btn { border-radius: 8px; }

/* :not() 排除;可传选择器列表 */
.btn:not(.primary, :disabled) { background: transparent; }

/* :has() 按「内部/后面有什么」反向筛元素 —— 父选择器与前置兄弟 */
.card:has(img) { padding: 0; }              /* 含图片的卡片 */
.card:has(> .tag.featured) { border: 1px solid #0066ff; }  /* 直接子级 */
.field:has(input:invalid) { color: #ee0a24; }              /* 表单整体标红 */
h2:has(+ h3) { margin-bottom: 4px; }        /* 后面紧跟 h3 的标题 */
⚠️
:has() 的三条硬规矩
① 参数是相对选择器列表,可以像 :has(> img)、:has(+ .next) 那样以组合器开头;② :has() 不能嵌套 :has(),参数里也不能放伪元素;③ 逗号表示「或」,链式 :has(a):has(b) 表示「且」。它自 2023 年 12 月起在主流浏览器广泛可用,但锚点别用 body、:root 或 *,内部条件尽量用 > / + 收紧范围,否则每次 DOM 变动都会触发大范围重算。

:is() 与 :where() 还有一个容易被忽略的区别::is() 接受容错选择器列表(forgiving selector list),参数里有一个浏览器不认识的选择器时,只丢弃那一个,其余照常生效;普通选择器列表只要有一项无效,整条规则作废。另外两者都不能把伪元素当参数。

伪元素:筛的是「元素的某一部分」

伪元素用双冒号,例如 ::before、::after、::first-line、::first-letter、::marker、::placeholder、::selection(本站另有《CSS 小技巧:::selection 自定义文本选中颜色》专文)。它们不在 HTML 里,却能被样式命中;::before / ::after 需要 content 才会生成。

css 伪元素常见用法
.required::after { content: " *"; color: #ee0a24; }
li::marker { color: #7c3aed; }
p::first-line { font-weight: 600; }
input::placeholder { color: #969799; }
::selection { background: #00d4ff; color: #0d1117; }

为什么样式没生效:优先级与层叠

选择器写对了却没生效,八成是优先级(specificity)没算对。MDN 把它定义为三列值 ID - CLASS - TYPE,从左到右逐列比较,不是十进制相加:哪怕写 150 个类型选择器,也赢不过 1 个类选择器。

列计入示例权重
IDID 选择器#app1-0-0
CLASS类、属性选择器、伪类.card [type=text] :hover0-1-0
TYPE类型选择器、伪元素p ::before0-0-1
不计数*、组合器、:where() 及其参数div > p0-0-0
css 算一算,谁赢
#app .list li.active { color: red; }        /* 1-2-1 */
.list li.active[data-id] { color: blue; }   /* 0-3-1 → 红胜:ID 列先比 */
:is(#app, .list) li { color: green; }       /* 1-0-1(取参数中最具体的 #app) */
:where(#app, .list) li { color: gray; }     /* 0-0-1(:where 恒为 0) */
.card:not(.disabled) { color: purple; }     /* 0-2-0(:not 取参数权重) */

完整裁决顺序是:来源与重要性 → 层叠层(@layer) → 优先级 → 作用域邻近性 → 源码顺序。几个关键结论:

  • 内联 style 可视为 1-0-0-0,压过普通作者样式;只有 !important 能覆盖它。
  • !important 不属于优先级,但会直接改变层叠比较:同源同层内,带它的声明无视优先级胜出;多个 !important 冲突时再比优先级。
  • @layer 里普通声明的优先级低于未分层的作者样式;层与层之间后声明的层优先。第三方样式推荐放进 @layer vendor,这样自己的业务样式不用靠 !important 去抢。
  • 只有前面全部相同,后写的才覆盖先写的;DOM 里元素离得远近不影响优先级,直接命中的样式也永远压过继承来的样式。
💡 排查四步:① 在开发者工具里确认选择器是否命中(Elements 面板看规则有没有被划掉);② 看是否被更高优先级覆盖;③ 看是否落在 @layer 里;④ 最后才看源码顺序。八成问题止于第 ② 步。

浏览器怎么匹配,以及性能边界

浏览器从右往左匹配候选元素,所以最右侧的选择器决定了初始候选集的大小。几条实用建议:

  • 别写 * .item 或深达五六层的后代链,先从具体类开始收窄。
  • 不要把 :has() 锚在 body / :root / * 上;内部条件用 > / + 限定,减少 DOM 变动时的子树重算。
  • 需要「很精确但不提高优先级」的约束时,把它塞进 :where(),这也是写基础样式/重置样式的常用手法。
  • 现代浏览器对常规选择器的优化已经很好,别为了微优化牺牲可读性;真正值得警惕的是 :has() 这类需要遍历子树的条件。

实战:一张卡片列表,全用选择器搞定

html 结构
<ul class="cards">
  <li class="card"><h3>基础篇</h3><p>内容</p></li>
  <li class="card"><img src="cover.jpg" alt="封面"><h3>带图卡片</h3></li>
  <li class="card featured"><h3>推荐</h3></li>
  <li class="card"></li>
</ul>
css cards.css
/* 基础层:用 :where 保持 0 优先级,方便业务覆盖 */
:where(.cards) { margin: 0; padding: 0; list-style: none; }

.cards .card {
  padding: 16px;
  border-bottom: 1px solid #ebedf0;
}
.cards .card:nth-child(odd) { background: #f7f9fc; }  /* 斑马纹 */
.cards .card:last-child { border-bottom: 0; }
.cards .card:hover { background: #eef5ff; }

/* 复合条件:既是推荐、又在首位 */
.cards .card.featured:first-child { border-left: 3px solid #0066ff; }

/* 含图片的卡片去掉内边距,图片铺满 */
.cards .card:has(> img) { padding: 0; }
.cards .card:has(> img) img { display: block; width: 100%; }

/* 空卡片:不出错,也不占位 */
.cards .card:empty { display: none; }

/* 没有标题的卡片给出兜底提示 */
.cards .card:not(:has(h3))::before {
  content: "暂无标题";
  color: #969799;
}

/* 键盘聚焦时给整张卡片加轮廓 */
.cards .card:has(:focus-visible) { outline: 2px solid #00d4ff; }

整段没有一行 JavaScript::nth-child 管条纹、:has() 做父级条件判断、:not() + ::before 做兜底、:where() 控制基础层不抢权重。这正是现代选择器组合使用的价值。

辨析:filter 是滤镜,不是选择器

如果你要找的其实是「把图片变灰、加模糊」的效果,那是 filter 属性,作用于已经命中的元素,属于视觉处理而非元素匹配。常用函数有 blur()、grayscale()、brightness()、contrast()、saturate()、hue-rotate()、invert()、sepia()、opacity()、drop-shadow(),多个函数按书写顺序依次应用。

css filter / backdrop-filter
.avatar--muted { filter: grayscale(1); }
.banner img { filter: brightness(.8) contrast(1.1); }
.icon { filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .25)); }  /* 沿图形轮廓投影,不支持 inset / spread */

/* backdrop-filter 处理的是元素「背后」的区域,常用于毛玻璃 */
.glass {
  background: rgba(255, 255, 255, .6);
  backdrop-filter: blur(12px) saturate(1.4);
}

一句话区分:选择器决定「谁被改」,filter 决定「改成什么视觉效果」;filter 的初始值 none 不继承,而选择器相关的层叠规则对它同样适用。

常见坑速查

现象原因处理
.a .b 写成 .a.b(或反过来)空格代表后代,无空格代表同一元素多条件先确认要的是「关系」还是「同时满足」
li:nth-child(2) 没命中想要的那一栏它数的是所有兄弟,不是同类型标签换 :nth-of-type(2) 或用 of 语法
多了个隐藏行,斑马纹错乱隐藏行仍参与编号:nth-child(even of :not([hidden]))
样式写了却没生效被更高优先级或 @layer 压过按「命中 → 优先级 → 层 → 顺序」排查
加了 !important 还是改不动冲突声明也带 !important,或来自更早的层去掉滥用,改用层叠层管理第三方样式
:is() 里写了一个不支持的选择器,整条规则失效?恰恰相反,它是容错列表,只丢弃那一项反过来:普通选择器列表才是一坏全坏
:has() 让页面滚动发涩锚点太宽、内部条件遍历整棵子树锚定具体容器,内部用 > / + 收紧
::before 不显示没写 content补 content: "" 或具体文本

总结

把 CSS 的「匹配」拆成三层就清楚了:选择器筛出元素,伪类与函数伪类补充状态与关系条件,优先级与层叠决定多条规则的最终胜负。再加一条边界——filter 属滤镜不属于选择器——这套知识就闭合了。

写 CSS 卡住时,先问「命中了吗」,再问「被谁盖了」。
—— 前者是选择器,后者是层叠,两件事别一起查