ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CSS伪类选择器完全指南:从hover到has,一文搞定状态与结构样式

CSS伪类选择器完全指南:从hover到has,一文搞定状态与结构样式 如果你写过几行CSS一定遇到过这种场景同一个按钮鼠标放上去要变色同一个表格隔一行要换个背景色同一个输入框聚焦时边框要亮起来。如果这些效果全部靠写类名去控制你得在HTML里加一大堆冗长的class还得用JavaScript监听各种事件繁琐得很。但CSS里其实有一类天生就为这种场景准备的工具——伪类选择器。这篇文章我就把这套东西从头到尾捋一遍从什么是伪类、常见伪类的用法、一些高级的函数式伪类到最后给你一张可以直接抄的速查表以及我踩过的坑。这套东西适合谁看刚入门的前端新手需要搞懂基础用法写了几年CSS但一直在用固定几个伪类的老手可以看看后面 :is、:where、:has 这些新东西怎么提效遇到“伪类不生效”问题的人可以直接跳到最后一章排查。我尽量少说废话多放能直接上手的例子。1. 先搞懂伪类选择器是什么以及它为什么值得你背下来1.1 所谓“伪类”伪在哪里要理解伪类先得理解“类”。平时我们写div classactive这个 active 是真实存在于HTML代码里的类名CSS 通过.active选中它这是“真类”。那伪类呢它描述的是一种不固定在HTML里的状态或位置。比如鼠标悬停在一个按钮上这个按钮在那一瞬间处于“被悬停”的状态但它不会因此在DOM里多出一个 class 属性。你没法在HTML源码里找到“悬停状态”的标记只能在CSS里用:hover去捕捉它。我常打一个比方一个人站在你面前他本身叫“张三”这是真实存在的类名。但他站立时是“站着的人”坐下时是“坐着的人”走路时是“走着的人”——这些状态不写在他的身份证上却真实影响你怎么称呼他。伪类就是CSS用来“称呼元素在不同时刻状态”的方式。它能解决的典型问题包括鼠标交互反馈、元素在父容器里的位置关系、表单控件的勾选与禁用状态、链接是否被访问过甚至还能根据“元素内部是否有内容”来决定显示什么样式。掌握伪类之后很多原本需要JS参与的活儿纯CSS就能完成页面性能更轻代码可读性也更好。1.2 伪类与伪元素一个冒号和两个冒号的区别新手最容易混淆的是把:hover这种伪类和::before、::after这种伪元素当成同类。它们长得像但完全是两码事。伪类用单个冒号比如:hover、:focus、:nth-child()它选中的是元素本身在某种状态下的样子。伪元素用两个冒号比如::before、::after、::first-line它选中的是元素上原本不存在的那个部分等价于在DOM渲染时“凭空捏”了一个子元素或一段文字。举个例子你写.btn::before { content: ; width: 8px; height: 8px; background: red; }浏览器会把每个.btn的视觉表现画成“按钮内部最前面有一个小红点”但这个红点不存在于HTML结构里只有CSS在渲染时才生成它。而如果你写.btn:hover { background: blue; }浏览器不会生成任何新元素它只是把现有的.btn在鼠标悬停时的背景色改成蓝色。这个区别决定了两者的作用域完全不同。伪类负责“状态切换”伪元素负责“内容虚构”。实际项目里两者经常配合使用比如后面要讲的涟漪光圈效果就是:hover这个伪类触发::before这个伪元素去执行动画。我自己的习惯是凡是老代码里出现单冒号的:before、:after会在重构时统一改成双冒号虽然老浏览器也认单冒号写法但按规范走能让代码更严谨。理解清楚这一点后面所有内容才能顺畅地进行下去。2. 交互状态必杀技hover、active、focus 的实战玩法2.1 :hover 远不止改个颜色鼠标移入事件交给CSS热搜词里有个高频搜索叫“css 鼠标移入事件”很多新手的第一反应是onmouseover事件绑定。实际上鼠标移入改样式这件事CSS 的 :hover 就能完成九成不需要写一行 JavaScript。随便来段代码感受一下.card { transition: transform 0.2s ease, box-shadow 0.2s ease; } .card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12); }这段代码的效果是鼠标移入卡片时卡片轻轻上浮并出现阴影。所有交互反馈都由:hover触发配合transition让变化过程平滑过渡。这种“卡片悬浮”效果在各类后台管理面板、商品列表里非常常见也是我认为:hover最值得掌握的用法之一。除了浮起效果:hover还能处理很多细节。比如搜索词里提到的“css 删除线”你想做一条删除线在文字上正常样式可以写.del { text-decoration: line-through; }但如果你希望在鼠标悬停时才显示删除线表示“这个选项即将被移除”.item { text-decoration: none; transition: text-decoration-color 0.2s; } .item:hover { text-decoration: line-through; text-decoration-color: red; }再比如导航菜单里的下划线滑动效果传统做法是鼠标移入时显示下划线很多人会用border-bottom但我更推荐用::after配合:hover做因为border-bottom会把布局撑出 1px 的偏差而::after是绝对定位不做布局干扰。.nav-link { position: relative; text-decoration: none; } .nav-link::after { content: ; position: absolute; left: 0; bottom: -4px; width: 100%; height: 2px; background: currentColor; transform: scaleX(0); transition: transform 0.25s ease; } .nav-link:hover::after { transform: scaleX(1); }这个效果就是鼠标移入时底部出现一条从左向右展开的横线很多网站的导航都是这么做的。注意一点::after的transform: scaleX(0)在一开始隐藏了横线:hover时把它恢复成原尺寸过渡动画才会平滑。这里必须提醒一个实战大坑移动端没有鼠标hover 状态在触屏上表现很诡异。你在手机上点一下某个元素:hover样式有可能会“粘”在元素上直到你点别处才消失。所以在做移动端项目时凡是按钮、卡片类组件一定要考虑如果没有 hover 反馈用户是否还能明白这里可以点击。常用的办法是用media (hover: hover)来限制 hover 效果只作用于支持悬停的设备media (hover: hover) { .card:hover { transform: translateY(-4px); } }实测下来这个写法在平板、手机上能避免大部分“点击后样式卡住”的诡异问题。2.2 :active 与 :focus点击和键盘操作的细节:hover负责鼠标悬停但很多人忽略了另外两个跟交互强相关的伪类:active和:focus。:active表示元素正在被激活的瞬间最常见的是鼠标按下去但还没松开的那短短几百毫秒。按钮按下时有一个向内压缩的效果就是典型的:active用法.btn { transition: transform 0.1s; } .btn:active { transform: scale(0.95); }这个反馈虽然细微但能让用户感觉到按钮真的被“按”了而不是生硬地响应点击。:focus表示元素获得焦点最常见的场景是输入框。配合热搜词里的“css中怎么把input居中”我可以延展讲一个常见布局在一个容器里让输入框水平垂直居中且聚焦时边框变色。.input-wrapper { display: flex; justify-content: center; align-items: center; min-height: 120px; } .input-wrapper input { width: 280px; padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; outline: none; transition: border-color 0.2s, box-shadow 0.2s; } .input-wrapper input:focus { border-color: #4a90d9; box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.25); }很多新手会在这类布局里写复杂的计算甚至JS去居中但实际上一个flex容器加上justify-content: center和align-items: center就解决了。而聚焦时的效果不用 JS 监听onfocus:focus一个伪类搞定。这里还有一个进阶伪类很值得提:focus-visible。它与:focus的区别是它只在键盘操作触发焦点时生效鼠标点击输入框时不会生效。为什么需要它因为鼠标点击输入框时浏览器默认会画一个丑丑的蓝色外圈这对鼠标用户来说是噪音但对键盘用户来说焦点指示是定位的依据不能去掉。:focus-visible就能在这个矛盾中找平衡input:focus-visible { outline: 2px solid #4a90d9; outline-offset: 2px; } input:focus { outline: none; }第一段规则让键盘用户用 Tab 聚焦时能看到清晰的外圈第二段规则让鼠标点击时不会出现默认外圈。这种细节看起来很微小但对键盘导航用户的使用体验影响非常大。做后台管理系统、表单页面时这个细节能直接提升可访问性评分。2.3 实战案例纯CSS实现涟漪光圈扩散效果搜索热词中出现过一个“css涟漪光圈扩散”这恰好是:hover结合伪元素做出来的经典效果。先看完整代码.ripple-btn { position: relative; overflow: hidden; padding: 10px 24px; border: none; background: #4a90d9; color: #fff; cursor: pointer; } .ripple-btn::before { content: ; position: absolute; left: 50%; top: 50%; width: 0; height: 0; border-radius: 50%; background: rgba(255, 255, 255, 0.4); transform: translate(-50%, -50%); transition: width 0.5s ease, height 0.5s ease, opacity 0.5s ease; opacity: 1; } .ripple-btn:hover::before { width: 200px; height: 200px; opacity: 0; }拆解一下原理。按钮内部有一个绝对定位的圆形伪元素::before初始宽度和高度都是 0位置通过left: 50%; top: 50%; transform: translate(-50%, -50%);固定在按钮正中心。鼠标移入时伪元素的宽高从 0 扩展到 200px同时透明度从 1 降到 0视觉上就是一个从中心向外扩散然后淡出的光圈。overflow: hidden是为了防止圆圈扩散到按钮边界之外。这个效果放在按钮、卡片、列表项上都挺出彩。你可以调整width、height的最终值来控制扩散范围调整transition的时长来控制速度。实际项目里我会把最终扩散的尺寸写成一个足够大的值比如按钮宽度的 2 倍以上确保光圈能从边缘露出来再消失。一个小细节:hover::before这种写法是“伪类伪元素”的组合选择器语义是“当按钮处于 hover 状态时改变其内部伪元素的样式”。这种写法不罕见但属于较高级的组合技巧理解它需要先理解前面说的“伪类是状态、伪元素是零件”。这个案例也回答了搜索结果里那个高频问题很多 CSS 动效并不是靠 JS 控制的动画库而是靠伪类切换状态、transition 做过渡最终呈现出的流畅交互。3. 结构性伪类不加类名也能精准定位元素3.1 first-child、last-child、nth-child 的日常用途交互伪类负责“状态”接下来这一类伪类负责“位置”。它们能在不加任何类名的情况下根据元素在父容器里的位置选中它们。最常见的场景是列表项去边框。你有一串列表每项之间有分隔线但最后一项的边框不应该出现。常规做法是给最后一项手动加类名稍微高级一点是最后一项用内联样式覆盖但都不如伪类优雅。ul classlist li项目一/li li项目二/li li项目三/li /ul.list li { padding: 12px 0; border-bottom: 1px solid #eee; } .list li:last-child { border-bottom: none; }:last-child选中的是父元素下最后一个子元素如果它的标签是li规则就生效。这样不管你中间新增多少li最后一项的边框始终能自动隐藏不需要维护额外类名。表格隔行变色也是结构伪类最经典的用法之一。以前有人喜欢手动给奇偶数行加不同类名其实一行CSS就能解决tbody tr:nth-child(even) { background: #f8f9fa; }even表示偶数行odd表示奇数行阅读性强人人都懂。这里必须说一下:first-child和:first-of-type的区别这是新手容易被绊倒的地方。:first-child要求元素同时满足“是父元素的第一个子元素”且“匹配选择器”:first-of-type要求元素是“父元素中同类型标签的第一个”。光说抽象直接看例子div classbox p第一段/p span第一个span/span p第二段/p /div如果写.box p:first-child选中的是第一段因为它是父容器里第一个子元素同时也是p。如果写.box span:first-child什么都选不中因为span虽然不是父容器第一个子元素。但如果写.box span:first-of-type选中的是那个span因为它是span类型元素里的第一个。这个区别在日常开发中非常容易踩坑。我见过不少同事在某个结构里写了p:first-child但因为前面藏了一个div或span而导致样式不生效排查了半天才发现是选择器理解有误。3.2 nth-child 公式到底怎么用一张表给你理清:nth-child()接受一个anb形式的参数这是很多人的难点。其实没那么复杂我直接用表格给你列清楚。写法选中结果说明:nth-child(3)第3个子元素固定序号:nth-child(odd)第1、3、5…个奇数位:nth-child(even)第2、4、6…个偶数位:nth-child(2n1)第1、3、5…个等价于 odd:nth-child(2n)第2、4、6…个等价于 even:nth-child(3n)第3、6、9…个每3个取第3个:nth-child(3n1)第1、4、7…个每3个取第1个:nth-child(-n3)第1至3个前3个元素为什么3n1会选中第 1、4、7 个因为n从 0 开始递增3*0113*1143*217以此类推。-n3则是因为当n3时结果为 0 或负数无效所以只剩n0,1,2时算出的 3、2、1。实际开发里栅格布局经常需要用到这些公式。比如一个九宫格每行三个你想给每行最后一个元素加右边距为0可以这样.grid-item:nth-child(3n) { margin-right: 0; }再比如轮播图下面的小圆点指示器你要让第 2、4、6、8 个小圆点颜色不同2n就能完成。我自己的经验是先在纸上把n0,1,2,...代进去算一遍前几个结果再对照预期效果基本不会错。3.3 :empty 与 :not两个容易被忽略的好帮手:empty代表“元素内部没有任何内容包括没有空白字符”。这个伪类的实用场景很典型后端渲染了空标签你想给它一个占位提示。.comment-box:empty::after { content: 暂无评论快来抢沙发吧; color: #999; }如果.comment-box里一个字符都没有页面就会自动显示这行提示文字不用后端判断、不用写JS。但要注意:empty对空白字符敏感如果HTML里写了换行和空格div classcomment-box /div这个 div 就不算 empty因为中间有换行和空格。很多人在这一步踩坑后抱怨:empty没用其实是 HTML 书写习惯导致的。解决的办法是让后端输出时别留空白或者干脆用 CSS 强制处理不建议为了 hack 而 hack最好还是在模板渲染时把空内容清干净。:not()是一个非常常用的否定伪类热度那么高不是没道理。它的作用是把满足括号内条件的元素排除掉。.form-item input:not(.no-validate) { border-color: #ddd; }这段的意思是所有.form-item里的输入框都要应用灰色边框但带着.no-validate类的除外。这种写法比“给每个非排除项单独加类名”要简洁得多尤其是在表单里有很多例外项时:not()能省掉大量冗余类名。还有一个小技巧结合前面说的搜索热词“css 两行超出...”当你想做“文字超过两行显示省略号”时经常需要判断哪一块内容是可能超出的。:not()加上结构伪类可以辅助实现更精准的样式控制比如最后一条内容不参与截断处理。.card-text { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .card-item:last-child .card-text { -webkit-line-clamp: unset; }最后一项卡片不限制行数其他项全部两行截断。这种差异化控制不靠类名纯靠位置伪类就完成了。4. 表单状态伪类纯CSS搞定表单交互反馈4.1 :checked 实现纯CSS开关按钮表单是网页跟用户打交道最频繁的区域而表单控件的状态天然适合使用伪类。搜索词里提到的“最酷的css tab控件”很多纯CSS Tab的实现核心就是:checked。先说基本原理。常见的写法是利用input typecheckbox与label的关联关系点击 label 时对应的 input 会被勾选或取消勾选:checked伪类可以感知这个变化再通过“兄弟选择器”把样式传递给旁边的元素。下面是经典开关按钮的实现label classswitch input typecheckbox hidden span classslider/span /label.switch { display: inline-block; width: 50px; height: 28px; border-radius: 20px; background: #ccc; transition: background 0.3s; cursor: pointer; position: relative; } .switch .slider { position: absolute; left: 3px; top: 3px; width: 22px; height: 22px; border-radius: 50%; background: #fff; transition: transform 0.3s; } .switch input:checked .slider { transform: translateX(22px); } .switch input:checked .slider { background: #4a90d9; }实际上最后一条规则有问题因为兄弟选择器是选中 input 后面的一个兄弟元素.slider是它的兄弟所以应该改成.switch input:checked ~ .slider { transform: translateX(22px); } .switch:has(input:checked) { background: #4a90d9; }前者把圆形滑块向右平移 22px后者用:has()让整个 label 背景变色。:has()是后面会细说的现代函数式伪类这里先混个脸熟。如果你不想用:has()也可以把背景色变化放在.slider本身上或者把 input 放在 label 最前面用精准选中它后面的兄弟元素再结合多重结构处理。总之:checked的价值在于它不需要 JS 去监听 checkbox 的 change 事件纯 CSS 就能完成状态切换。我在实际项目里用这个思路做过整套 Tab 切换、折叠面板、导航菜单高亮。只要布局上满足了 “input 在兄弟元素前面” 这个条件纯 CSS 完全可以撑起这些小交互而且切换零延迟性能极好。4.2 :disabled、:required、:invalid 的表单状态:disabled可以给禁用的输入框或按钮一个明确的视觉标识。最常见的写法是input:disabled { background: #f5f5f5; cursor: not-allowed; opacity: 0.6; }cursor: not-allowed会让鼠标变成“禁止”图标对用户来说这是最直接的告知方式。但这里有个细节要注意光有cursor不够如果输入框本身还绑定了点击事件即使加了 disabled用户还是可能困惑为什么点上去没有反应。建议在禁用态时把pointer-events: none也加上彻底屏蔽所有鼠标事件。这样虽然不能解决 JS 层面的事件问题但至少交互层不会出现误导。:required和:invalid是配合 HTML 表单验证属性使用的伪类。:required能选中带required属性的表单项:invalid能选中“当前校验失败”的表单项。比较经典的场景是输入框校验失败时边框变红input:invalid { border-color: #e74c3c; }但这里有个很坑的地方输入框刚渲染时如果为空且带 required它会立即命中 :invalid导致页面一打开就出现一堆红框。这不是你想要的。处理办法有两个一是用:not(:placeholder-shown)结合二是等用户交互后再显示错误。后面:placeholder-shown的章节会讲第一种办法。:user-invalid是比较好用的新伪类它表示“用户已经交互过且校验失败”的元素。Chrome 和 Firefox 都支持了safari 支持稍慢。如果你不想自己维护“是否已经输入过”的状态:user-invalid是个好选择input:user-invalid { border-color: #e74c3c; box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.2); }这个伪类只有用户真正操作过表单项之后才会触发校验样式有效避免了页面初始时的满屏红框问题。4.3 :placeholder-shown 实现浮动标签:placeholder-shown的含义是“占位文字正在显示”也就是说输入框目前没有真实内容显示的是 placeholder。一旦用户输入了任何字符placeholder 消失这个伪类就不再匹配。利用这个特性可以做出非常优雅的“浮动标签”效果。标签文字作为 input 的 placeholder 显示初始状态它出现在输入框内部输入后它“浮”到输入框上方腾出空间给用户输入的内容。我先给基础 HTML 和 CSS。div classfield input classfield-input placeholder请输入邮箱 / label classfield-label邮箱/label /div.field { position: relative; } .field-input { width: 100%; padding: 18px 12px 6px; border: 1px solid #ccc; border-radius: 6px; outline: none; font-size: 16px; } .field-label { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: #999; font-size: 14px; transition: all 0.2s ease; pointer-events: none; } .field-input:not(:placeholder-shown) .field-label { top: 8px; transform: none; font-size: 12px; color: #4a90d9; }核心规则是最后一段当 input 的 placeholder 已经不再显示说明用户输入了内容此时 label 从输入框垂直居中位置移动到顶部缩小的位置。如果不加:not()写成.field-input:placeholder-shown .field-label效果就会反过来跟预期完全相反。:placeholder-shown对现代浏览器支持很好可以用来做很多输入框状态联动。比如 placeholder 消失时同时给父容器加一个高亮边框表示“正在输入中”或者“输入完成”。这类交互用 JS 监听也是能实现的但 CSS 的响应没有延迟、没有事件绑定成本代码还更短。我个人的偏好是凡是能用 CSS 状态表达的逻辑优先用 CSS。5. 现代函数式伪类is、where、has 提升效率5.1 :is() 与 :where()告别又臭又长的逗号选择器写过大型项目的人一定经历过这种折磨一个样式要同时作用于多个选择器选择器列表长得能排三行。比如统一设置各标题的字体大小h1, .title-1, .section-title, .banner h1 { font-size: 32px; }:is()可以把这些选择器收拢到一个括号里:is(h1, .title-1, .section-title, .banner h1) { font-size: 32px; }一眼看过去:is()的写法信息密度更高当你需要给这一组选择器追加其他规则时不用再重复复制一长串逗号列表。:where()跟:is()长得几乎一模一样唯一的区别是优先级。:is()的优先级取参数列表里优先级最高的那个选择器而:where()的优先级永远是 0。这个区别在覆盖样式时特别有用。举个例子你想设置所有卡片内的标题为 24px但允许业务代码随时用.featured-title覆盖它:where(.card, .panel, .modal) h2 { font-size: 24px; }由于:where()完全不贡献优先级后面任何一条写.card h2 { font-size: 28px; }的规则都能覆盖它不需要为了比优先级去追加更长的选择器。反过来如果你希望某个基础规则“强硬”一点不想被别人轻易覆盖就用:is()因为它的优先级可能会压过后面的普通选择器。5.2 :has()CSS终于可以“找爸爸”了:has()是这些年 CSS 选择器领域最振奋人心的更新之一。它允许你根据子元素、后续兄弟元素或任何内部状态去选中父元素通俗说就是“如果里面有某样东西就把外面这个容器选中”。最典型的应用场景卡片内部有一个“推荐”标签时整张卡片加一个高亮边框。.card:has(.badge) { border-color: #f39c12; }这个选择器的语义是“选中任何带有.badge子元素的.card”。这在过去几乎必须用 JS 去查 DOM 才能实现现在纯 CSS 就完成了。再比如前面讲开关按钮时想让整个 label 在 checkbox 勾选后变蓝色:has()也能优雅解决.switch:has(input:checked) { background: #4a90d9; }这里的input:checked是:has()里的参数表示“容器内存在一个被勾选的 input”于是整个 label 命中规则。这种写法比“用兄弟选择器一层层找”直观得多。:has()还能配合兄弟选择器解决更复杂的布局问题。比如一个表单中当某个输入框校验失败时它后面跟的错误提示需要显示出来.field:has(input:invalid) .error-msg { display: block; } .field:has(input:valid) .success-msg { display: block; }一条规则就把整个“错误状态显示”解决了。5.3 功能伪类的组合用法与降级建议这几个函数式伪类最大的魅力在于可以互相嵌套组合。比如.card:has(:is(.tag, .badge, .label)) { padding: 16px; }这句话的意思是只要卡片内部有.tag、.badge或.label中的任何一个就给它加 16px 内边距。:has()负责向上找父容器:is()负责聚合多个条件写起来非常干净。再复杂一点你想排除某些内部条件.card:has(.cover) .card-content { padding-left: 0; } .card:not(:has(.cover)) .card-content { padding-left: 16px; }:not()与:has()嵌套形成了“有封面图”和“没有封面图”两种布局分支。逻辑会复杂一些但能力确实强大了很多。不过我要泼一盆冷水:has()的兼容性虽然在近几年快速补上了但如果你的项目需要支持特别老的一套浏览器比如某些政务内网、老旧客户端:has()还是可能白屏。不要把所有布局逻辑都押在它身上。我的建议是“渐进增强”核心布局用传统选择器完成:has()只作为锦上添花的增强层。比如边框高亮、间距微调这类不影响整体结构的样式放心用:has()但如果某个组件的整体显隐、关键布局完全依赖:has()就要谨慎评估兼容范围。6. 速查表与排坑实录6.1 伪类选择器速查表按功能分类前面每一章都是讲用法这里给你一张可以直接保存的速查表。我按功能把常用的伪类分成了五组写代码时不确定某个伪类怎么拼、什么时候生效翻出来扫一眼就行。分类伪类触发时机交互状态:hover鼠标悬停元素上交互状态:active鼠标按下且尚未松开交互状态:focus元素获得焦点鼠标点击或Tab键交互状态:focus-visible键盘操作触发焦点时交互状态:link尚未访问过的链接交互状态:visited访问过的链接结构位置:first-child父元素下的第一个子元素结构位置:last-child父元素下的最后一个子元素结构位置:nth-child(n)父元素下第n个子元素结构位置:nth-of-type(n)父元素下第n个同类型元素结构位置:first-of-type父元素下同类型中的第一个结构位置:not(sel)不匹配sel的元素结构位置:empty没有任何内容含空白字符的元素表单:checked勾选状态的input表单:disabled禁用状态的表单控件表单:required带required属性的表单控件表单:invalid内容校验失败的输入框表单:valid内容校验成功的输入框表单:placeholder-shownplaceholder正在显示函数式:is(sel1, sel2)参数中任意选择器匹配即生效函数式:where(sel1, sel2)同 :is 但优先级为0函数式:has(sel)内部存在匹配sel的元素这张表里没有列全所有伪类比如:target、:lang()、:root这些在特定场景下也有用但日常项目里上面这些已经覆盖了九成以上的需求。:target我简单提一句它的作用是当时 URL 中的锚点指向某个元素时生效做单页文档高亮很好用但用得确实不多。6.2 伪类不生效先按这份清单排查伪类不生效是出现频率最高的问题我自己也被坑过很多次总结下来无外乎以下几个原因。第一选择器拼写错误或空格错误。:hover写成:howver:nth-child写成:nth-child (2n)多了一个空格整条规则直接失效。CSS 选择器对空格非常敏感:nth-child(2n)能起作用:nth-child (2n)就是非法写法。遇到不生效的第一反应应该是去检查选择器本身。第二父级结构不满足匹配条件。比如用:first-child时目标元素不是父容器的第一个子元素前面可能藏了一个看不见的空白节点、注释节点或者其他标签。这种情况我会先在 DevTools 里展开元素看完整结构确认它在 DOM 树里的真实位置。第三优先级被其他样式覆盖。伪类也是选择器也遵循优先级规则。:hover写的背景色可能被后面一条更高优先级的background规则覆盖了这时候不是“伪类没生效”而是“生效了但被盖住了”。检查方式是在 DevTools 的 Styles 面板里看哪条规则加了删除线并把鼠标悬停元素上看实时计算后的样式。第四元素根本不支持该伪类。比如:checked用在div上div不是可勾选元素:checked永远不会匹配。:invalid用在没有校验约束的输入框上也不会触发。用错伪类相当于对应关系不存在样式自然出不来。第五移动端 hover 粘滞。前面提过触屏设备上的:hover效果表现跟桌面端完全不同点击后样式卡住不消失或者完全没反应。解决方案是给交互组件加上media (hover: hover)条件或者直接不做 hover 效果改用:active。我列了一张排查清单方便你在现场按顺序检查现象排查方向解决建议选择器不生效检查拼写与空格对照速查表重新书写首尾项样式异常确认是否是第一个子元素改用 first-of-type 或加类名表格隔行颜色没变检查是否被其他背景覆盖查看优先级与覆盖规则表单验证样式过早出现空输入框命中 invalid配合 :placeholder-shown 或使用 :user-invalid移动端点选后样式粘住hover 状态残留用 media (hover: hover) 限制点击按钮无反馈没写 :active 或样式被覆盖加 :active 规则并检查优先级第六:has() 兼容性问题。:has()在老版本浏览器里完全不生效如果你发现某些页面用户反馈“样式丢了”第一时间就要想到是不是用了太新的选择器语法。检查方式非常粗暴用 DevTools 模拟一个低版本浏览器内核或者直接查 Can I Use。第七忘记加触发条件。有些人写:checked相关样式时HTML 里没有对应的 checkbox或者 label 跟 input 不是“显式或隐式关联”的状态也会导致伪类永远不匹配。:checked必须作用于可勾选控件上并且这个控件必须真的能被用户切换状态。排查伪类问题我习惯的思路是先在浏览器 DevTools 里看元素是否被选中检查匹配的选择器是否出现在 Styles 面板再计算最终样式值最后一步才怀疑伪类书写错误。这样定位问题最快能避免对着代码空想。6.3 兼容性怎么处理伪类选择器的兼容性其实比很多人想象中好。:hover、:active、:focus、:nth-child这些东西从 IE 时代就在用完全不用担心。新增的函数式伪类才是需要关注的焦点。:is()和:where()在现代浏览器里支持度很高但如果你要兼容的是某个非常旧的环境建议还是用传统的逗号选择器兜底。:has()是这几个里面最年轻的虽然桌面端主流浏览器已经支持但某些 WebView 或旧版本 Safari 可能没有。处理方式可以参考我前面的建议把:has()当作增强层不是核心依赖。这里要特别提醒一个认识误区PostCSS 或 Autoprefixer 这类工具通常不会帮你转换选择器语法。它们主要处理属性前缀比如-webkit-transform但:has()能不能用、要不要降级需要你自己在代码里做判断。所以不要以为加了个构建工具就万事大吉还是要对项目实际需要支持的浏览器版本有清晰了解。最后说一个实际项目里的技巧我经常把这类“渐进增强”规则集中放在一个文件里比如enhance.css用supports包裹。当浏览器支持:has()时才加载这部分样式supports selector(:has(*)) { .card:has(.badge) { border-color: #f39c12; } }这样即使某些浏览器不支持:has()也只是缺少这一小段增强样式永远不会因为选择器语法错误导致整个样式表解析失败。一点实际操作下来的体会这篇文章讲了很多规则、样式、代码但说到底学会伪类选择器最关键的是思维方式的转变写页面样式时先想清楚“这个样式是跟随状态变化还是跟随内容位置变化”再决定是用:hover、:nth-child、:checked还是直接加类名。我写 CSS 的时间越久越倾向于用语义清晰的伪类去表达交互状态而不是在 HTML 里疯狂堆砌 class。最后再分享一个小习惯日常写代码时保持自己的 CSS 结构清爽选择器不要写成一长串嵌套能用后代的伪类组合时尽量用组合。比如“三行模式”的写法——选择器一行、声明块一行、结束符一行——本身对代码风格没有强制要求但当你使用了大量伪类选择器之后代码本来就比普通类选择器更紧凑保持结构整齐会更容易排查问题。伪类选择器不是炫技它是让你更少写代码、更少维护状态、更优雅表达交互的工具。希望这篇指南能让你在实际项目里少走几步弯路。
返回列表