本节摘要:基础选择器是"按名字找人",组合选择器和属性选择器是"按关系和特征找人"。组合选择器利用 DOM 树的父子兄弟四类关系缩小范围,属性选择器按 HTML 属性的存在与否、值的精确匹配与前缀后缀包含来筛选元素——处理表单、链接列表时它们能省掉大量多余的类名。
完成本节后,你将能够:使用四种组合选择器根据元素层级关系定位目标;使用七种属性选择器根据 HTML 属性特征筛选元素;用"相邻兄弟"技巧实现无需类名的自动间距;在真实页面里减少对多余 class 的依赖。
HTML 文档是一棵 DOM 树,元素之间存在父子、兄弟等层级关系。组合选择器利用这些关系来缩小选择范围——很多时候你不需要给目标元素加类名,它的"位置"本身就说明了它是谁。
后代选择器用空格分隔两个选择器,选中前者内部的所有后者元素——不论嵌套多深。
nav a { color: white; text-decoration: none; }
这条规则选中 <nav> 内部的所有 <a> 元素,无论 <a> 是直接子元素还是嵌套在多层 <ul>、<li> 里面。后代选择器是使用频率最高的组合选择器,特别适合"限定范围"的语义:只在导航里的链接才去下划线,正文链接不受影响。
子选择器用 > 分隔,只选中直接子元素,不关心更深层级的后代。
.menu > li { list-style: none; padding: 8px 0; }
<ul class="menu"> <li>直接子元素 — 会被选中</li> <li> 直接子元素 — 会被选中 <ul> <li>孙子元素 — 不会被选中</li> </ul> </li> </ul>
子选择器和后代选择器的区别在于"深度"。当你只想影响直接子元素而不波及嵌套的同名元素时,子选择器是正确选择。上面的例子中,如果误用了后代选择器 .menu li,嵌套子列表里的 <li> 也会被去掉项目符号——而下拉子菜单往往需要保留自己的符号或缩进。
相邻兄弟选择器用 + 分隔,选中紧接在前者之后的第一个后者元素。
h2 + p { font-size: 18px; color: #555; }
这条规则只选中紧跟在 <h2> 后面的第一个 <p> 元素。如果 <h2> 后面跟的是 <div> 而不是 <p>,则不会有任何元素被选中。它的经典用途是"文章导语":标题后的第一段通常是摘要性质,加大字号、变灰,之后段落恢复正文样式——不用给导语单独加类。
通用兄弟选择器用 ~ 分隔,选中前者之后的所有同级后者元素(不要求紧邻)。
h2 ~ p { line-height: 1.8; }
这条规则选中 <h2> 之后的所有同级 <p> 元素,中间可以隔着其他元素。注意它只往"后"看——<h2> 之前的段落不会被选中;而且只在同一父元素的子元素之间生效,跨层级无效。
组合选择器在 DOM 树片段中的命中范围
| 选择器 | 符号 | 关系 | 示例 | 匹配范围 |
|---|---|---|---|---|
| 后代 | 空格 | 任意深度后代 | nav a |
nav 内所有 a |
| 子 | > |
直接子元素 | ul > li |
ul 的直接 li 子元素 |
| 相邻兄弟 | + |
紧邻的下一个兄弟 | h2 + p |
h2 后面紧邻的 p |
| 通用兄弟 | ~ |
之后的所有兄弟 | h2 ~ p |
h2 后面所有同级 p |
纸上得来终觉浅,做一张"实验卡片"直接看差异。HTML 结构:
<div class="lab"> <h3>标题</h3> <p>第一段(紧邻标题)</p> <p>第二段</p> <blockquote>一段引用</blockquote> <p>第三段(在引用之后)</p> <div class="inner"> <p>嵌套在 inner 里的段落</p> </div> </div>
依次应用四条规则,逐一注释刷新,记录命中情况:
/* 实验一:.lab p —— 命中 lab 内全部 4 个 p(含嵌套) */ /* 实验二:.lab > p —— 只命中 lab 的直接子 p(3 个,不含 inner 里那个) */ /* 实验三:h3 + p —— 只命中"第一段" */ /* 实验四:h3 ~ p —— 命中第一、二、三段(inner 里的不算,因为它是 h3 的"侄辈"不是兄弟) */
四个实验做完,"深度"和"兄弟"两个维度就刻进脑子了。给每条规则加上明显的背景色会让对比更醒目,比如在实验三里加 background: #FFF9C4。
属性选择器根据 HTML 元素的属性或属性值来筛选元素。这在处理表单元素时特别有用——你可以根据 type、name、placeholder 等属性来精准定位,完全不需要额外加类。
[attr] 选中所有包含该属性的元素,不管属性值是什么。
[disabled] { opacity: 0.5; cursor: not-allowed; } [required] { border-left: 3px solid #F44336; }
两行代码就完成了"所有禁用控件半透明、所有必填字段标红杠"的表单可用性增强——不需要给任何元素加类,属性本身就是语义。
[attr="value"] 选中属性值完全等于指定值的元素。
input[type="text"] { border: 1px solid #ccc; padding: 8px; } input[type="submit"] { background-color: #1976D2; color: white; border: none; padding: 8px 24px; }
表单是属性选择器的主场:同样是 input,text、checkbox、radio、submit 长得完全不同,靠 type 属性区分比加一堆类干净得多。
CSS 提供了多种基于属性值片段的匹配方式,每种都有独特的匹配规则。
/* [attr~="val"] — 属性值中包含以空格分隔的完整词语 val */ [class~="warning"] { color: #D32F2F; } /* 匹配 class="alert warning" 但不匹配 class="warnings" */ /* [attr|="val"] — 属性值等于 val 或以 val 加连字符开头 */ [lang|="zh"] { font-family: "Microsoft YaHei", sans-serif; } /* 匹配 lang="zh" 和 lang="zh-CN" */ /* [attr^="val"] — 属性值以 val 开头 */ a[href^="https"] { padding-left: 20px; } /* 选中所有 https 开头的链接 */ /* [attr$="val"] — 属性值以 val 结尾 */ a[href$=".pdf"] { padding-right: 20px; } /* 选中所有 PDF 文件链接 */ /* [attr*="val"] — 属性值中包含 val 子串 */ input[name*="email"] { border-color: #1976D2; } /* 选中 name 中包含 email 的输入框,如 user_email、email_backup */
六个符号容易记混,给一个口诀:波浪线找"整词",竖线找"语言",尖号看头,美元看尾,星号看身上。其中 ^= 和 $= 日常最常用——配合下一节的伪元素,可以给外部链接加小图标、给 PDF 链接加文件标识,全部纯 CSS 实现。
| 选择器 | 匹配规则 | 典型用途 |
|---|---|---|
[attr] |
包含该属性 | 标记必填/禁用字段 |
[attr="val"] |
精确等于 | 区分表单 input 类型 |
[attr~="val"] |
空格分隔的词列表包含 val | 类名式匹配 |
[attr|="val"] |
等于 val 或以 val 加连字符开头 | 语言属性匹配 |
[attr^="val"] |
以 val 开头 | 链接协议筛选 |
[attr$="val"] |
以 val 结尾 | 文件类型筛选 |
[attr*="val"] |
包含 val 子串 | 模糊匹配 |
注意 ~= 与 *= 的差别:前者要求"完整的词"(按空格切分后比对),后者只要"子串"。class="alert warning" 能被 [class~="warning"] 和 [class*="warn"] 同时命中,但 [class~="warn"] 落空——warn 不是一个完整的词。
在实际项目中,组合选择器和属性选择器经常搭配使用,形成精确的选择逻辑。
/* 导航栏中的直接链接(不匹配下拉菜单里的链接) */ .nav > li > a { color: white; padding: 12px 16px; } /* 表单中所有文本类输入框共用一套样式 */ form input[type="text"], form input[type="email"], form input[type="password"] { border: 1px solid #ccc; border-radius: 4px; padding: 8px 12px; } /* 文章区域中 h2 后面的第一个段落做导语 */ .article h2 + p { font-size: 18px; color: #666; font-style: italic; } /* 复选框和单选框不占文本框的样式 */ form input[type="checkbox"], form input[type="radio"] { width: auto; }
第二个例子值得展开:表单控件类型多,逐个加类繁琐又容易漏,input[type="..."] 让"控件长什么样"跟着"控件是什么"走,这正是 CSS 倡导的语义化思路。
继续个人主页项目。给页头加一个导航,再加一个"留言"表单,全用本节的选择器搞定样式,一个新类名都不加:
<nav class="site-nav"> <ul class="menu"> <li><a href="#home">首页</a></li> <li><a href="#posts">文章</a></li> <li> <a href="#works">作品</a> <ul class="submenu"> <li><a href="#css">CSS 练习</a></li> <li><a href="#layout">布局实验</a></li> </ul> </li> </ul> </nav> <form class="guestbook"> <input type="text" name="nickname" placeholder="昵称" required> <input type="email" name="email" placeholder="邮箱"> <textarea name="message" required placeholder="留言内容"></textarea> <input type="submit" value="提交"> </form>
/* 只选中一级菜单项,子菜单不受影响 */ .menu > li { display: inline-block; margin-right: 8px; } .menu > li > a { color: #1565C0; text-decoration: none; padding: 6px 10px; } /* 子菜单垂直排列 */ .menu .submenu li a { color: #555; font-size: 14px; } /* 表单:文本类控件统一外观 */ .guestbook input[type="text"], .guestbook input[type="email"], .guestbook textarea { display: block; width: 100%; max-width: 360px; border: 1px solid #ccc; border-radius: 4px; padding: 8px 12px; margin-bottom: 12px; } /* 必填字段左侧红杠,禁用字段半透明 */ .guestbook [required] { border-left: 3px solid #D32F2F; } /* 提交按钮独立成军 */ .guestbook input[type="submit"] { background: #1565C0; color: #fff; border: none; padding: 10px 28px; border-radius: 4px; cursor: pointer; }
对照检查:子菜单链接是灰色小字(.menu .submenu li a 后代选择命中),一级导航是蓝色(.menu > li > a 子选择限定深度);昵称和留言框有红杠([required]),邮箱没有;提交按钮没有继承文本框的全宽(type="submit" 被排除在分组外)。然后做个破坏实验:把 .menu > li > a 的 > 改成空格,子菜单链接也变蓝变大——深度失控了。改回来,你就真正掌握了本节。
💡 动手建议:打开一个你喜欢的网站,在浏览器开发者工具中尝试用
document.querySelectorAll()测试各种组合选择器和属性选择器。比如document.querySelectorAll('nav > a')或document.querySelectorAll('input[type="text"]'),观察返回的结果是否符合预期。
⚠️ 性能提醒:后代选择器(空格)在大型 DOM 树中可能导致浏览器需要遍历大量节点。在性能敏感的场景中(如滚动事件触发的样式变化),尽量使用子选择器
>或类选择器来缩小匹配范围。另外,选择器是从右往左匹配的——.menu .submenu li a会先找出页面上所有a再逐级验证祖先,最右侧的选择器越具体,整体越快。
问:后代选择器和子选择器,日常该默认用哪个? 默认倾向子选择器 >。它的边界清晰——只影响一层,嵌套组件不会被误伤;后代选择器是"深水炸弹",命中范围随 DOM 加深而扩大,组件化项目里最容易踩的就是"样式莫名其妙泄漏到子组件"。当然,做"范围内全部生效"的主题类样式时空格仍是正解,按意图选择。
问:相邻兄弟选择器能往前选吗? 不能。+ 和 ~ 都只向后看,CSS 没有向前匹配的兄弟选择器。想根据后面的元素状态改前面的元素,传统方案是把视觉上"在前面"的元素放到 DOM 的后面,再用别的方式摆回原位;新方案有 :has() 这种"父先知"选择器,它按子元素或后邻元素的状态选中前面的容器,属于较新的特性,进阶时值得专门了解。
问:属性选择器能匹配 class 吗? 能,[class*="card"] 会命中所有 class 里含 card 的元素。但几乎总是不如直接写 .card——类选择器语义更明确、性能更好。属性匹配 class 的合理场景是"按类的开头分段筛选",比如 [class^="icon-"] 选中所有图标类前缀的元素,配合自动化脚本生成样式时偶尔有用。
问:为什么我的 h2 + p 没命中? 九成是因为中间隔了东西——+ 要求严格紧邻。标题和段落之间哪怕塞了一个看不见的空标签或换行渲染出的匿名盒子(其实文本节点不影响,但一个 <br> 或 <span> 会),紧邻关系就断了。改用 h2 ~ p 能容忍间隔,但要确认你确实想命中后面所有段落。
这一节的技巧合起来指向一个方向:让 HTML 保持干净。回顾刚才的个人主页导航与表单——我们没有往任何元素上新增类,样式却精确落位了:一级菜单靠子选择器限定深度,必填字段靠属性选择器标记,导语靠相邻兄弟定位。这些"位置与特征"本来就已经写在 HTML 里,属性选择器只是把它们重新利用了一遍。
类名更少带来三层实际收益。HTML 更小更可读,翻结构时不会被几十个类名晃花眼;改结构更安全,挪动一个元素时不必同步搬运它的类;团队协作更顺,结构评审时讨论的是语义而不是"这个类是干嘛的"。当然也不要走极端——强行用超长的组合选择器消灭所有类,可读性反而崩坏。平衡点是:结构性的语义交给标签和属性,可复用的外观组件才用类。
判断"该加类还是用组合选择器"有一条朴素标准:如果这段选择逻辑将来会在别处复用,起个类名;如果它描述的是"这个位置上的这个东西",组合选择器更贴切。前者是给重复模式命名,后者是描述一次性的上下文——两种思路各得其所。
>(直接子元素)、+(紧邻的下一个兄弟)、~(之后所有同级兄弟)。下一节登场的是 CSS 里最有创意的两类选择器——伪类捕获元素的"状态",伪元素凭空创造"虚拟内容"。