
学习CSS选择器这事儿吧说简单也简单说复杂它真的能复杂到让你怀疑人生。我见过不少前端新人HTML标签背得滚瓜烂熟CSS却只会用class一个页面写下来满屏的div classbox1 box2 box3改样式靠猜加需求靠补最后样式表膨胀得跟毛线团一样。其实很多问题的根源不是CSS属性记不住而是选择器没学透——你压根不知道怎么精准地圈中想要的那个元素。我最早学CSS的时候也走过这个弯路。当时觉得选择器嘛不就是.class和#id吗够用了。直到后来做项目要处理复杂嵌套结构、做表格斑马纹、做表单状态反馈才发现自己一直在用最笨的方式写样式加一堆无意义的class甚至用Javascript去操作样式。后来把选择器系统地补了一遍回头看以前写的代码真想把当时的自己拽过来打一顿。这篇就当作一个完整的选择器学习笔记吧从基础到进阶从用法到原理把我踩过的坑和总结的技巧都放进去适合刚入门前端的朋友也适合那些已经写了一阵子但想系统梳理一遍的同学。1. 选择器到底是什么为什么说它是CSS的地基1.1 选择器的作用从整页样式到指哪打哪先聊个生活化的比喻。CSS说白了就是给网页做装修选择器就是你的点名册。你手里有一堆装修工人样式声明比如颜色改成红色字号放大到16px加个圆角边框但你不能对着整栋楼喊所有人把衣服换成红色你得点名——哪面墙刷红漆哪扇门装什么把手。选择器干的就是这个点名的活儿。CSS的整套规则其实就是两个部分选择器 { 属性: 值; }选择器负责找谁声明块负责改什么。两者缺一不可但恰恰是前半部分经常被人忽视。原因也简单新手阶段接触的页面结构单一div加个class就能解决问题选择器的价值还没暴露出来。等页面复杂了嵌套深了、组件多了、要响应各种交互状态了你才发现怎么把样式精准作用到我想作用的那一个元素上是最核心的能力。1.2 没有选择器CSS代码会变成什么灾难你可以想象一下如果没有选择器或者只会用最基础的标签选择器CSS会变成什么样。你写了一个p { color: red; }页面上所有段落全变红。想单独改某一个段落不好意思做不到只能再去套一层内联样式或者给那个段落单独加个class。结果就是HTML里塞满了style维护起来简直是一场噩梦。选择器本质上是一种匹配规则它告诉浏览器满足这些条件的元素把后面的样式给我套上。条件可以是一个标签名、一个class、一个id、元素之间的位置关系、元素的属性值、甚至是元素当前的状态。条件越精确你的样式就越可控条件组合得越合理你的代码就越简洁。所以我把学习选择器这件事当成CSS入门的第一道分水岭。过不了这道坎后面什么flex、grid、动画、响应式写出来都是浆糊。2. 三类基础选择器标签、类、ID选择器2.1 元素选择器最朴素但最容易被误用元素选择器就是直接用HTML标签名作为选择器比如p { line-height: 1.6; } a { color: blue; }它的特点是范围极大匹配页面上所有同类型的标签。有人觉得它很low其实不是。元素选择器最适合用来做基础样式兜底也就是所谓的reset和默认样式。比如你希望全站所有段落都有统一的line-height所有链接都没有下划线用元素选择器是最省事的。但问题也随之而来它的作用范围太广了。你要是在一个复杂的组件化项目里用元素选择器去精调某个特定区域的样式很容易殃及池鱼。举个例子你只想改首页第一个banner区域里链接的颜色如果直接写a { color: yellow; }那全站链接都跟着黄了。所以元素选择器的使用场景应该是全局统一风格而不是局部微调。我在项目中见过一种反面教材——为了改某个页面的样式直接给div加了一堆全局属性结果整个项目的布局全乱了最后加班排查到凌晨。那次的教训我记到现在元素选择器是范围伤害使用前必须想清楚它的辐射面。2.2 类选择器前端开发最常用的主力类选择器用.开头比如.card {}。它命中的是HTML中class属性里包含该值的所有元素div classcard卡片内容/div.card { border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); }类选择器之所以最常用是因为它可以重复使用、组合使用语义足够灵活。一个元素可以同时挂在多个class下面比如div classcard card--active这就为样式组合和状态控制留出了很大的空间。你写Vue、React组件的时候类名也是最常见的样式挂载点。类选择器命名这块我强烈建议从一开始就养成习惯。别用.left、.red、.box1这种只描述外观的类名要面向语义命名——.product-card、.nav-item、.form-input。不然页面复杂了以后满屏的.left和.box3你自己看了都头疼。2.3 ID选择器能用但别当主武器ID选择器用#开头匹配文档中唯一的元素#header { height: 60px; }在HTML规范里一个页面的ID是唯一的所以ID选择器天生就是精确制导。但它有两个问题让我不是很推荐把它当主力。第一是复用性太差。你设计了一套卡片组件想让它出现三次用ID直接就不可能了。第二是优先级过高。ID选择器的优先级远高于类选择器一旦你写了一条#header .title { color: red }后面任何想通过类选择器覆盖的顺序都无效。这会导致样式覆盖困难改起来极其痛苦。ID选择器比较合理的用途是页面锚点定位、某些必须是唯一整体区域的样式锚点比如某个独立的布局容器、以及在写JavaScript时的getElementById钩子。样式的精细化控制尽量交给类选择器。2.4 优先级三个等级叠加的权重计算聊完三类基础选择器必须把优先级这个核心机制说清楚。不然你写代码的时候会出现明明我写了怎么没生效的灵异事件。CSS优先级最基本的比较规则可以简化成ID选择器的个数 类选择器的个数 元素选择器的个数。同级数量相同的情况下写在后面的会覆盖前面的这里默认不讨论!important和行内样式。举个例子/* 优先级一个类 两个元素 */ .nav li a { color: blue; } /* 优先级一个类 一个元素 */ .nav a { color: red; }第二个选择器优先级更高因为元素选择器的数量上面是2个下面是1个。虽然第一段写在后面但实际生效的是第二段。这种计算方式新手最容易迷糊。我的经验是尽量别去写超过三层深的嵌套选择器比如.wrap .content .box .item .text层级越深优先级越复杂后面覆盖就越难。3. 组合与关系选择器搞定元素之间的亲戚关系3.1 后代选择器空格键连接的所有子孙后代选择器是最常见的组合方式语法上就是两个选择器之间用空格分隔.article p { color: #333; }这个规则匹配的是.article内部所有的p标签不管是直接子级还是嵌套在更深层级的子子孙孙全部命中。这个选择器的优点是覆盖面广缺点是容易误伤。我举个实际例子。你有一个文章容器.article里面有一段引言用了特殊样式div classarticle p正文第一段/p div classquote p这是引言样式应该不一样/p /div /div如果你用.article p { color: #333 }引言里的p也会变成正文颜色。你不得不再写一条.article .quote p { color: #999 }去覆盖。选择器嵌套深度又加了一层优先级又变高了代码越来越难管。所以用后代选择器的时候脑子里一定要有一张DOM结构图想清楚它会影响到哪些层级的元素。3.2 子元素选择器用精确控制直接下属子元素选择器用连接只匹配直接的子元素不会穿透到孙子层级.article p { color: #333; }还是上面的HTML这条规则只会命中正文第一段那个直接子级的p引言里的p不在范围内了因为你用的是.quote p的形式而那层是包在.quote下面的。子元素选择器在日常布局里特别有用。比如做导航栏菜单项下面有子菜单你想给一级菜单的每个直接子项加边距又不想影响二级菜单的排版:scope 相关的写法用起来就很优雅。早点养成能限定就限定的习惯你的样式就不会四处飞了。3.3 兄弟选择器和~控制前后邻居兄弟选择器有两种。一种是相邻兄弟选择器用连接匹配紧跟在某个元素后面的那个特定元素h2 p { color: teal; }这条规则的意思是紧跟在h2后面的那个p会被命中。如果h2和p中间隔了其他元素就不生效。这种写法在做标题下面的第一段需要特殊间距时特别顺手不用额外加class。另一种是通用兄弟选择器用~连接匹配某元素后面的所有符合条件的兄弟元素h2 ~ p { color: gray; }这个命中的是同一个父级下面h2后面的所有p不管中间隔了什么。兄弟选择器在实现状态联动时很有用。比如某个复选框:checked了后面的表单元素要变成可编辑状态不需要JavaScript直接靠选择器就能搞定。3.4 一个综合案例列表项的错落感说了这么多干巴巴的规则我放一个实际能用的例子。假设你要做一组横向标签列表希望每个标签之间有间距而且最后一个标签不要有右边距ul classtags li classtag前端/li li classtagCSS/li li classtag选择器/li li classtag实战/li /ul.tag .tag { margin-left: 12px; }这样写的效果是所有挨着前面标签的标签都有左边距第一个标签没有完美的间隙控制。你没用一个多余的class也没写什么first-child特判代码干净利落。这就是关系选择器最爽的地方——用结构关系来表达样式意图而不是到处写死类名。4. 伪类选择器状态与位置的隐形助手4.1 用户状态伪类hover、focus和active的真实身份伪类选择器是CSS选择器里最有意思的一类它们有点像是动态的类名不需要你在HTML里手动添加浏览器会根据元素的状态自动匹配。最常遇到的就是用户交互三兄弟:hover鼠标悬停时匹配:focus元素获得焦点时匹配比如点击输入框后:active元素正被按下时匹配它们的典型用法button:hover { background-color: #0056b3; } button:active { transform: scale(0.98); } input:focus { border-color: #3478f6; outline: none; box-shadow: 0 0 0 3px rgba(52, 118, 246, 0.2); }这里有一个我踩过的坑很多人只写了:hover忘了写:focus导致键盘用户用Tab键切换到按钮时没有任何视觉反馈。可访问性里这属于硬伤——视力正常的用户能鼠标悬停看到变化键盘用户完全摸瞎。所以写交互样式时我习惯 hover 和 focus 一起写用逗号隔开共用一段样式。还有一个细节:active不只是能在a和button上用任何可点击元素都可以比如做卡片点击效果时给整块卡片加:active变形。4.2 结构性伪类first-child、nth-child与last-child结构性伪类是按位置选元素的利器。它们按元素在DOM里的位置关系来匹配不需要添加任何辅助类名。:first-child和:last-child比较好理解分别匹配父元素下第一个和最后一个子元素。还有个容易混淆的是:first-of-type匹配兄弟元素中同类型的第一个。下面这两段就有本质区别/* 父元素下第一个 p 元素 */ p:first-child { font-weight: bold; } /* 同类型 p 元素里的第一个 */ p:first-of-type { font-weight: bold; }如果父元素下第一个子元素是span那p:first-child根本匹配不到任何元素因为p不是第一个子元素但p:first-of-type依然可以命中p元素中排在第一个的那个。:nth-child()就更强大了它的参数可以是一个数字、一个关键字odd奇数even偶数或一个函数形式的表达式比如2n1、3n。它的核心是按父元素下的所有子元素的顺序来计数然后再判断这个元素是不是对应的类型。4.3 实战斑马纹表格的两种写法对比做斑马纹表格奇数行、偶数行背景色不同有几种方式用来对比结构性伪类再合适不过。第一种最朴素的方式是在HTML里给每个奇数行手动加classtr classodd/tr tr/tr tr classodd/tr写一个就罢了几百行的表格你手动加想都不要想。第二种用:nth-child()tbody tr:nth-child(odd) { background-color: #f9fafb; } tbody tr:nth-child(even) { background-color: #ffffff; }一段样式解决所有问题数据再多也不用管不需要改HTML。这就是结构性伪类在现代CSS里的地位把样式表达从人工标注变成结构推导。不过这里也有一个经典坑位:nth-child计数的是所有子元素不是只有某一种标签。假如你的表格里某个tr底下混了td和th有些场景下你用td:nth-child(2)会发现自己选中的不一定是第二个td可能是第二个子元素如果第一个子元素恰好是th的话。要更精确地按同类型兄弟中的第几个得用:nth-of-type()。这两个的差别面试高频实用中更容易翻车。4.4 表单状态伪类enabled、checked、required等等表单元素是伪类使用的重灾区因为它们的交互状态特别丰富。:checked表示复选框或单选框被选中:disabled表示不可用:enabled表示可用:required表示必填项。举个例子用:checked实现一个纯净版的自定义开关.switch { width: 40px; height: 24px; background-color: #ccc; border-radius: 999px; transition: background-color 0.2s; } .switch:has(input:checked) { background-color: #3478f6; }从写法上看现代CSS还引入了诸如:has()这样的关系型伪类可以根据是否包含某元素去匹配父级这个等后面细聊但它真的是给表单交互开了很大的挂。5. 属性选择器和伪元素选择器发掘更多隐藏玩法5.1 属性选择器的六种匹配模式属性选择器是通过元素的属性名和属性值来匹配的写法上比较灵活。它一共有六种常见的匹配模式[attr]只要元素有该属性就匹配不管属性值是什么[attrvalue]属性值完全等于value[attr^value]属性值以value开头[attr$value]属性值以value结尾[attr*value]属性值包含value[attr~value]属性值是以空格分隔的单词列表之一这里最容易让人记混的是^开头、$结尾、*包含这三个。我当时的记忆方法是看符号本身^像不像从左边翘起来$是正则表达式里常见的结尾锚点*表示任意包含。实际应用场景很典型a[href^https://] { color: green; } a[href$.pdf] { background: url(pdf-icon.svg) no-repeat; } img[alt] { border-radius: 4px; }我以前的团队做文档站点时就用a[href^http]区分外链然后给外链自动加个小箭头图标一行CSS搞定完全没有JavaScript。5.2 伪元素选择器::before和::after的核心用法伪元素选择器用双冒号::开头常见的有::before、::after、::first-line、::first-letter和::placeholder。注意单冒号是伪类双冒号是伪元素这个从CSS3开始规范化了老代码里为了兼容有些写单冒号的但你写新代码就统一双冒号吧。::before和::after本质上是在元素内容的前面或后面创造了一个虚拟盒子。它俩必须有content属性哪怕content: 也得写上不然根本不显示。它们的使用场景多到数不过来给模块加小图标、做角标、做气泡尾巴、做hover延伸线。.card::after { content: ; position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; transition: opacity 0.3s; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15); } .card:hover::after { opacity: 1; }上面这段就利用伪元素做了一层层淡淡阴影的hover效果而 HTML 结构完全没动。这也是开发中我最常用的技巧之一。还有一个新增的实用伪元素::selection它可以定义用户选中一段文字时的高亮颜色。不要小看这个细节很多内容站点靠它来强化品牌色比如说用户选中正文时背景变成品牌蓝、文字保持白色阅读感受会明显加分。6. 选择器的组合、优先级陷阱与性能优化6.1 选择器列表用逗号合并重复规则写CSS时经常遇到多个选择器共用同一段样式的情况比如h1, h2, h3 { font-family: PingFang SC, Microsoft YaHei, sans-serif; line-height: 1.4; }这就是选择器列表。注意它和后代选择器的空格不是一回事逗号分隔的是每个选择器各自独立匹配空格分隔的是组合匹配。很多新手会把.card, .title { color: red }和.card .title { color: red }搞混前者是两个独立的类各自变红后者是.card内部的.title变红作用范围差得远了。还有一种容易忽视的情况选择器列表里如果某个选择器本身有错误在旧浏览器里整个规则会被丢弃。所以写的时候尽量把简单的选择器放前面复杂的放后面降低整体失效风险。6.2 优先级计算的完整体系和!important的正确打开方式前面简单提过优先级这里稍微系统化一下。CSS优先级按从高到低大体是声明带有!important行内样式写在style属性里ID选择器的数量类、属性、伪类的数量元素、伪元素的数量通配符*和继承样式最低这个顺序意味着你写了一条行内color: red外部样式表里无论怎么用ID选择器都很难覆盖除非也加!important。所以工程上对行内样式要严防死守对!important更要慎用慎用再慎用。它像是“我不管上下文我就要这么干”的霸王条款用多了样式系统直接回归石器时代。我见过最惨烈的例子一个老项目里到处是!important到了后面想改任何一个样式唯一办法是再写一条带!important的规则把它压掉然后等着哪天又冒出新需求接着叠。整个样式文件变成了比谁后出场、谁更霸道的斗兽场。合理的使用场景我认为只有两类一是覆盖第三方组件库的样式确实优先级不够了又不方便改组件源码二是做用户端主题换肤时需要强制应用某个全局变量值。日常业务样式别碰!important。6.3 选择器性能浏览器是真的一行一行去匹配吗很多初学者会问CSS选择器会影响页面性能吗这个问题的答案要分两层。第一层现代浏览器的样式匹配机制并不是拿到一个选择器就去遍历整棵DOM树而是从右往左匹配——先找选择器最右边那部分的元素再逐级向上验证祖先节点。也就是说ul li这个选择器浏览器是先找所有li再去检查它们的父级往上是不是ul。所以最右边的部分如果特别宽泛类似* {}、div {}匹配的量就大性能相对差。第二层在现代网页的实际体量下纯选择器匹配的性能开销其实很小。除非你有几万个DOM节点而且选择器嵌套很深否则你很难感知到选择器导致的卡顿。真正拖垮性能的往往是重排重绘、大量动画、图片解码之类的问题。但即便如此我还是建议养成两个习惯一是避免过深的后代选择器嵌套比如.sidebar .widget .box .content a {}这种四级五级的写法又难读又难覆盖二是避免用*做全局匹配的复杂选择器比如* { box-sizing: border-box }没毛病但写个body * .title就没必要了。6.4 可维护性给选择器建立命名契约前面零零碎碎提到命名问题这里集中说一个工程上的心得。写CSS选择器本质上是在给样式模块建立契约。你的类名越像是业务语义别人看代码就越容易理解。我比较推荐BEM类命名法的思想——虽然不是一定要全套照搬但它的核心思路很值得借鉴模块外层的类叫.card模块内部的子元素叫.card__title、.card__content模块的不同状态或变体叫.card--active、.card--small这种命名的好处是你一眼就知道.card__title只能出现在.card内部.card--active表示卡片处于激活状态。选择器写起来配合也会很舒服.card__title { font-size: 18px; } .card--active { border-color: #3478f6; }有人觉得BEM类名又长又啰嗦但对比一下什么.wrap、.main、.content、.box这种看起来干净实则含义不明的命名方式真的到了大型项目里BEM反而是一种救命稻草。类名长一点换来的是可以完全脱离 父级选择器 表达样式依赖关系清晰移动和重构都轻松。7. 进阶玩法:not()与:has()的实际应用7.1 用:not()做排除让代码少写一半:not()是取反选择器括号里传入需要排除的选择器。它的实际能力比表面看起来强得多因为括号里可以是复杂选择器。最常见的应用是处理最后一个元素不要间距这类问题但写法上比last-child更灵活。比如.item:not(:last-child) { margin-bottom: 16px; }这段的效果是除了最后一个.item之外所有.item都有下边距。以前我们怎么处理给最后一个加一个.last类或者用:last-child专门清掉 margin都麻烦通过:not()直接就解决了。还可以组合使用实现一些逻辑上的当且仅当效果比如input:not(:placeholder-shown) { border-color: #333; }这条匹配的是有内容输入的输入框当用户输入了内容时给输入框加深边框。不用JavaScript就能完成表单状态的视觉反馈这在很多验证类页面里非常实用。:required、:invalid以及:placeholder-shown这些表单状态配合:not()能组合出许多交互细节。7.2:has()能选父元素的新时代选择器:has()大概是最近几年CSS选择器领域里程碑级别的更新。它让选择器可以基于内部是否包含某元素来反向匹配父级也就是俗称的父选择器。它的语法就是括号里放进一个相对选择器如果匹配则当前元素被选中。一个最典型的例子还是表单校验反馈.form-control:has(input:invalid) { border-color: red; }这个规则表达的意思是任何div classform-control只要它内部包含一个状态为:invalid的输入框那么这个div就显示红色边框。这在以前几乎必须写JavaScript监听输入事件再切换class现在靠样式层就完成了。再比如对整个卡片做联动.card:has(img) .card__content { padding-top: 0; }如果卡片内部有图片内容区就取消顶部内边距。逻辑非常直观。这里必须提醒一下兼容性——不要太兴奋。:has()在现代浏览器Chrome 105、Safari 15.4、Firefox 121里表现良好但如果你还在维护老版本浏览器用户占一定比例的项目用之前务必确认目标浏览器范围。它的性能开销也比普通选择器高一些不建议在一个页面的关键路径上大量堆叠。7.3 选择器里的作用域思想再往外延伸一点现代CSS发展和组件化开发非常同步。在Web Components、Vue scoped样式和CSS Modules普遍使用之后选择器已经不单单是选元素了它还承担了隔离样式的作用。比如在Vue的scoped样式里框架会自动给组件的元素加一个特殊属性>body div:last-child { padding-bottom: 60px; }这个选择器的意图是针对某个特定页面容器的底部留白结果它匹配到了所有body下最后一个div——而那个div恰好是全局页脚。页脚下面凭空多了60px内边距整个页面显示出来就像底部有块空白。这就是过度使用后代选择器和位置选择器而没进行作用域约束的结果。后来把它改成带上专属类名的写法问题立刻消失。这种坑你靠肉眼有时候真的一时半会儿发现不了。我后来排查样式问题时习惯先把所有涉及到的选择器列出来逐个分析它的匹配范围特别是那种带 div:last-child、:not()之类的组合选择器范围比直觉上大得多。8.3 给新手的实操建议三步吃透选择器如果你正在学选择器我给个不太一样的建议路线不要只看文档学完就上手多做效果差很多。第一步随便找个现成网页用浏览器开发者工具的选择元素功能把每个元素的真实选择器路径都看一遍。强烈推荐在Elements面板里右键元素选择Copy然后观察它生成的CSS选择器。看它是怎么命中这个元素的为什么命中都靠哪些祖先关系。第二步把一些常见的样式交互通过不加新class的方式实现——比如合计用:nth-child和:not完成列表的间距处理用:hover和::after做按钮的悬浮动效。练熟之后你会发现很多以前要写JavaScript和新增类名的事情CSS自己就能解决。第三步在CodePen那样的在线工具里自己造一组复杂的DOM结构然后尝试用不同的选择器去选中不同的元素写完之后用浏览器的选中元素高亮验证一下看看自己理解得到不到位。这比单纯背选择器列表有用得多。选择器的语法就那么几条真正的深度在于你能不能在大脑中勾勒出DOM树并把选择器的匹配范围转换成视觉上的高亮区域。9. 结尾说点我自己的使用体会写了这么多最后聊几句实在的。我在日常开发里选择器使用的频率大概是这样的类选择器占大头接近七成关系选择器和伪类选择器占两成多ID选择器和属性选择器零星出现。这个比例说明一个问题所有CSS选择器本质上都是为你精准定位元素这个目标服务的而类选择器之所以占比高是因为它作为钩子可读性最高、语义最清晰、可复用性最好。我建议你学选择器的时候不要抱着我要把所有写法都背下来的心态而是带着我要搞清楚这个页面元素究竟该怎么命中的思路去学。掌握了这些之后你写出的CSS会明显清爽很多CSS文件的长度往往也能压缩不少排查样式问题时定位也会快很多。如果觉得追赶这些新特性吃力也不必焦虑。把最基础、最核心的选择器类、ID、元素、后代、子元素、常用的几个伪类和属性选择器吃透日常绝大多数需求就能覆盖了。进阶特性是锦上添花等有需要再钻研一点都不迟。