CSS 可以改变颜色、字号和间距,也可以用一条规则管理一整类元素。页面变大以后,逐个修改节点很快就会失控。更合适的做法是先用选择器说清楚目标范围,再把同一组样式交给所有匹配的元素。

我们先从类型选择器看起,再讨论类、ID、属性匹配和文档树组合器。遇到长选择器时,可以先记住一条实用的阅读方法:最右侧选择器决定最终要选中的元素,左侧部分负责限定它所处的条件。
选择器不会“差不多匹配”。一个元素要么满足完整条件,要么不满足。调试 CSS 时,我们先确认它有没有选中目标,再检查层叠、继承和优先级,排查会更直接。
先看一条最常见的 CSS 规则。它由选择器和声明块组成:
h2 {
color: #475569;
}这里的 h2 是选择器,表示所有 h2 元素;花括号里的 color: #475569; 是声明。以后页面里再增加一个 h2,它也会自动应用这条规则。这样写的好处很实际:修改一次规则就能更新所有二级标题,不需要把同一个 style 属性复制到每个节点里。
类型选择器通常就是 HTML 元素名,例如 html、p、em、a。它按节点类型进行匹配:
html { color: #0f172a; }
h1 { color: #334155; }
p { color: #64748b; }这种写法并不只属于 HTML。在 XML 或自定义标记语言中,只要名称对应文档树里的某种元素类型,也可以直接用作类型选择器。
我们常用类型选择器设置正文、标题和链接的默认外观。如果只想强调一部分段落,p 的范围就太大了,这时再用类、属性或结构条件把目标缩小。
如果几种元素需要共用一组声明,可以用逗号把选择器列在一起:
h1,
h2,
h3,
p.lead {
color: #4338ca;
}列表中的每一项仍然是独立选择器。浏览器分别匹配 h1、h2、h3 和 p.lead,然后把同一个声明块应用到这些结果上。这里的逗号不能随手省掉:h2, p 表示“所有二级标题和所有段落”,h2 p 则只找“位于二级标题内部的段落”。少一个逗号,选择器的结构就变了。
一个声明块里也可以放多条声明:
h1 {
color: #312e81;
background: #eef2ff;
padding: 0.5em;
border: 1px solid #c7d2fe;
}每条声明都建议以分号结束。最后一条的分号通常可以省略,不过保留它更省心:以后追加属性时,不容易忘记给上一条补分号。假如中间漏写分号,例如 color: purple background: aqua;,解析器会把后半段继续当作 color 的值。整个值无法解析,这条声明就会被忽略;前面已经正确结束的声明仍然有效。
开发时把声明写成清楚的多行格式即可。删除空白不会改变规则含义,但这类压缩适合交给构建工具,不必拿源文件的可读性去换。
通配选择器 * 会匹配当前范围内的任意元素:
* {
box-sizing: border-box;
}你可以把它理解为一份包含所有元素类型的超长列表。* 写起来很省事,影响范围也确实很大。给所有元素统一设置边框盒模型通常符合预期;如果统一设置粗体、颜色或外边距,就很可能牵连到不相关的节点。使用前先确认需求是不是当真指向“所有元素”。
选择器列表和多条声明可以放在同一条规则里:
h1,
h2,
h3,
h4,
h5,
h6 {
color: #334155;
background: #fff;
padding: 0.5em;
border-bottom: 1px solid #cbd5e1;
font-family: system-ui, sans-serif;
}这段代码会让左侧列出的每种标题都接收右侧的全部声明。分组表达的是“这些目标在这里使用同一套样式”,不意味着相同声明必须全部合并。如果两个目标只是暂时长得一样,语义和后续调整方向并不相同,把它们绑在一起反而会增加修改成本。
你可以点击按钮切换选择器,看看哪些元素会被高亮。实验的样式和脚本都封装在 iframe 内,不会改动正文页面。
如果目标横跨多种元素类型,或者只对应某一种业务角色,可以在标记中添加 class:
<p class="warning">库存即将耗尽。</p>
<span class="warning">请立即处理</span>在类名前加上点号,就是类选择器:
.warning {
color: #b91c1c;
font-weight: 700;
}.warning 没有限制元素类型,所以上面的两个节点都会匹配。如果只想选择警告段落,可以把类型和类直接连起来,写成 p.warning。中间不能加空格:p.warning 要求同一个元素既是 p,又拥有 warning 类;p .warning 找的却是 p 内部带有 warning 类的后代。
*.warning 和 .warning 的效果相同,通常直接省略 *。类名最好描述稳定的语义,并避开数字开头的写法。特殊开头虽然可以转义,但代码更难读,也更容易遇到工具链兼容问题。
一个元素的 class 属性可以包含用空格分隔的多个词:
<p class="notice urgent">服务将在十分钟后暂停。</p>其中每个词都是独立类名,排列顺序不影响匹配。我们可以先为各个类设置基础样式,再用连续的类选择器表示交集:
.notice { font-weight: 700; }
.urgent { color: #dc2626; }
.notice.urgent { background: #fef2f2; }.notice.urgent 中间没有空格,它要求同一个元素同时拥有这两个类。继续写成 .notice.urgent.pinned 后,三个类就必须全部存在,缺少任何一个都不会匹配。类名的先后次序不影响结果,但大小写要与文档保持一致。
这种组合很适合描述组件状态。例如 .button.primary.disabled 表示一个处于禁用状态的主操作按钮,不必再为这组状态创造一个很长的新类名。
ID 选择器以 # 开头,对应元素的 id 属性:
<main id="primary-content">...</main>#primary-content {
max-width: 72rem;
margin-inline: auto;
}同一个文档里,一个 ID 值只应该出现一次。重复 ID 时,浏览器的 CSS 匹配或许仍能给多个节点上色,但文档已经不规范。getElementById()、锚点定位和无障碍关联也可能因此得到含糊的结果。换句话说,样式暂时能显示,不代表标记写对了。
需要复用样式、表示类别或状态时,用 class 更合适;需要页面内唯一标识、锚点或控件关联时,再使用 id。ID 的样式优先级权重也更高,如果只是复用外观,没有必要增加这层约束。它不能像 class 那样保存多个用空格分隔的身份,也不该通过重复写 #target#target 人为抬高权重。
类名和 ID 值都要留意大小写:.criticalInfo 不会匹配 class="criticalinfo"。换到其他标记语言后,点号类语法能不能使用,取决于该语言怎样定义类语义;如果没有相同的类机制,还可以改用属性选择器。

方括号可以根据属性是否存在来选择元素:
img[alt] { outline: 2px solid #16a34a; }
a[href][title] { text-decoration-thickness: 2px; }img[alt] 只检查图片有没有 alt 属性,不在意属性值是什么。连续写 [href][title],则要求两个属性同时存在。开发阶段可以用这种方式做诊断,比如标出已经填写替代文本的图片,或者区分真正有 href 的链接和只用来占位的 a 元素。
加上等号以后,属性值必须精确相等:
input[type="email"] { border-color: #2563eb; }
a[href="/account/settings"] { font-weight: 700; }精确匹配比较的是完整值。协议、路径、空格或单词顺序发生变化,都可能匹配失败。这里尤其容易混淆的是 class:p[class="urgent warning"] 要求整个 class 字符串恰好等于这个值;p.warning.urgent 只检查两个类是否存在,不在意先后顺序,也允许元素拥有其他类。
属性值建议统一加上引号。值中出现空格、特殊字符,或者以数字、连字符开头时,引号本来就可能是必需的。统一写法也能少踩一些边界问题。
完整属性值不固定时,可以根据需要选择不同的部分匹配运算符:
~= 寻找空格分隔的完整词,*= 寻找任意位置的子串。假设属性是 class="cloudy barren",[class~="cloud"] 不会匹配,因为列表里没有完整单词 cloud;[class*="cloud"] 则可以匹配,因为 cloudy 确实包含这段字符。
[lang|="zh"] { quotes: "“" "”"; }
img[title~="示意图"] { border: 1px solid #94a3b8; }
a[href*="example.com"] { font-weight: 700; }
a[href^="mailto:"] { font-style: italic; }
a[不要把 |= 简单理解成“开头匹配”。它要求属性值完全相等,或者匹配内容后面紧接一个连字符。因此 [lang|="en"] 能匹配 en 和 en-US,不能匹配 english 或 cy-en。语言标签和带连字符的命名体系正好适合这种规则。
多个属性条件也可以连续组合:
[class|="btn"][class*="-arrow"] {
padding-inline-end: 2rem;
}这条规则要求 class 值等于 btn 或以 btn- 开始,并且还要在某处包含 -arrow。部分匹配很灵活,也容易误选。例如 [href*="example.com"] 只检查字符串,并不理解 URL 的主机边界,所以不能把 CSS 选择器当作安全或业务判断依据。

i 标记属性值是否区分大小写,要看文档语言和具体属性怎样定义。HTML 中 type="checkbox" 这类枚举关键字通常不区分大小写;class、id、title 和普通 URL 字符串通常按区分大小写处理。某个属性的规则不能直接套到其他属性上。
如果希望某一次属性值匹配明确忽略大小写,可以在右方括号前加上 i:
a[href$=".pdf" i] {
font-weight: 700;
}这样就能匹配以 .pdf、.PDF、.Pdf 等形式结尾的值。i 只改变方括号里属性值的比较方式。在区分大小写的 XML 中,它不会让属性名 type 和 TYPE 变成同一个名字。属性值怎样比较,与属性名怎样识别,是两套规则。
还有一种用于强制区分大小写的 s 标记方案,但在没有确认目标浏览器支持情况前,不适合依赖它。实际项目里,先把数据格式统一,再在确实需要忽略大小写的地方使用 i,结果会更可控。
选好运算符和值后,实验会实时标出匹配到的卡片。你可以重点比较 ~= 和 *=,再看看区分大小写与忽略大小写时结果有什么变化。
浏览器会把 HTML 组织成一棵文档树。直接包含另一个元素的节点叫父元素,直接被包含的节点叫子元素;处在同一个父元素下的多个子元素互为兄弟。跨过一层或多层向上追溯,会遇到祖先;向下展开,则是后代。
<main>
<section>
<p>阅读 <a href="/guide"><strong>完整指南</strong></a></p>
</section>
</main>放到这棵树里看,p 是 a 的父元素,也是 strong 的祖先;a 才是 strong 的直接父元素。父子一定属于祖先与后代,但祖先与后代之间可能隔着很多层。组合器就是用来把这些结构条件写进选择器的。
html 是 HTML 文档的根元素,body 是页面可见内容的主要祖先。判断节点关系时,要看元素树,不能只凭代码缩进或屏幕上的位置。CSS 布局可以改变视觉顺序,但不会顺手改写 DOM 中的兄弟关系。

选择器中的空格表示后代组合器:
h1 em { color: #64748b; }
.sidebar a { color: #fff; }h1 em 会匹配 h1 内任意深度的 em。阅读这类选择器时,可以从右向左看:先找到所有 em,再留下祖先链中有 h1 的节点。两者中间隔一层还是很多层,都不影响这次匹配。
后代选择器可以连续写很多层,比如 ul ol ul em。条件越长,样式与当前 DOM 深层结构绑得越紧。实际项目中,通常只保留足以表达语义的条件,不必把现有 DOM 的每一层都抄进选择器。
大于号 > 表示直接子代关系:
h1 > strong { color: #dc2626; }
table.summary td > p { margin-block: 0; }h1 > strong 只匹配直接放在 h1 下面的 strong;中间多一层 span,条件就不成立。table.summary td > p 同时用了后代和子代条件:最终目标是 p,它必须是 td 的直接子元素,而这个 td 又要位于带有 summary 类的 table 中。
还有一点经常被误解:后代选择器不比较“哪个祖先离目标更近”。如果一个 span 同时满足 div:not(.help) span 和 div.help span,两条规则都会匹配。最终采用哪条样式,要看优先级和源码顺序,祖先与目标之间的距离不参与判断。
加号 + 是相邻兄弟组合器。它选择同一父元素下,紧跟在前一个元素之后的那个元素:
h1 + p { margin-top: 0; }
li + li { border-top: 1px solid #e2e8f0; }h1 + p 只能选中紧接在 h1 后面的 p,不能反过来选中 h1。li + li 会匹配每组列表中从第二项开始的 li,因为第一项前面没有同父级的 li。
元素之间夹着纯文本节点时,+ 仍然可以匹配,因为组合器比较的是元素兄弟。如果把那段文字包进 p,它就变成了元素,也会阻断原来的相邻关系。两边的节点还必须共享同一个父元素;分属不同列表的 li 即使在画面上挨得很近,也不算兄弟。
波浪号 ~ 表示后续兄弟:
h2 ~ ol { font-style: italic; }它会选择同一父元素下,出现在 h2 后面的所有 ol,不要求两者紧挨着。相邻兄弟也属于后续兄弟,所以两个元素紧邻时,可能同时匹配 + 和 ~;中间隔着其他元素时,就只能匹配 ~。
这些组合器也可以混合使用:
html > body table + ul {
margin-top: 1.5em;
}从右向左读这条规则:先找到 ul,它必须紧跟在某个 table 后面;这个 table 位于 body 中;body 又必须是 html 的直接子元素。
点击四种组合器,就能看到目标节点怎样受到结构边界限制。可以特别留意:+ 只找紧邻的一个节点,~ 则可能找到多个后续兄弟。
复杂选择器没有按预期工作时,先别急着提高优先级或添加 !important。我们可以按下面的顺序,一步步找到问题出在哪儿:
从最右侧开始,在开发者工具里单独测试目标选择器。例如先确认 .item 能不能选中预期节点。
再从右向左逐段加回结构条件,例如依次测试 section .item、.panel section .item,看看加入哪一段后匹配数变成了零。
核对点号、井号、方括号、逗号和组合器。.a.b 表示同一元素上的类交集,.a .b 表示后代,.a, .b 表示并集。三者只差一个字符,含义却完全不同。
还有几类错误值得单独检查:重复使用 ID 来代替 class;用 [class="..."] 精确匹配包含多个类的字符串;把 *= 误当成完整词匹配;认为 DOM 中离目标更近的祖先权重更高;把视觉上的相邻当成元素兄弟;或者让选择器过长,严重依赖当前 DOM 层级。
写选择器时,我们真正需要表达的是组件角色和必要结构,不是把整棵 DOM 树复刻一遍。只要目标明确、条件够用,而且匹配边界可以验证,短一些的选择器通常更容易调整。
* 匹配任意元素,写起来简单,使用时要先确认影响边界。>、+、~ 分别表示任意层后代、直接子代、紧邻兄弟和所有后续兄弟。说到底,一个选择器就在回答两个问题:要给谁设置样式,以及它必须处在什么上下文中。写完后也可以按这两个问题反向检查:最右侧是不是目标,左侧条件是不是确有必要。这样既能控制匹配范围,也不会让选择器无端变长。
确认选择器已经匹配后,再去检查层叠、继承、源码顺序和属性覆盖。先把匹配问题与样式冲突问题分开,判断会更清楚。