ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CSS伪类选择器实战指南:从状态、结构到表单的完整攻略

CSS伪类选择器实战指南:从状态、结构到表单的完整攻略 写CSS写了这么多年要说最常用却最容易被低估的选择器我肯定投票给伪类。它不像类选择器那样直白也不像伪元素那样能凭空变出装饰内容但页面里那些细腻的交互反馈、结构化的列表样式、表单的即时校验提示几乎全靠它在背后撑场子。经常有刚入门的朋友问我“鼠标移上去变色怎么做”“表格隔行变色有没有简单的办法”“输入框内容不合法时怎么标红”这些问题十有八九都能用伪类选择器解决甚至一行CSS都不用写JS。这篇指南我结合日常项目的真实用法把常用伪类按类别拆开讲清楚每个知识点都配了可直接抄的代码最后还会聊一聊我在实际开发里踩过的坑。无论你是刚学会基础选择器的新手还是想系统梳理伪类用法的老手这份实战笔记应该都能帮上忙。文章有点长建议收藏遇到选择器相关的样式问题时翻出来对照着查。1. 伪类选择器到底在选什么——先建立整体认知1.1 伪类不是伪元素这两者可别搞混很多初学者会把:hover和::before混为一谈因为它们都带冒号长得确实挺像。但这两者本质上解决的问题完全不同伪类描述的是“元素在某种状态下的样子”比如鼠标悬停、输入框聚焦、列表第n项伪元素则是“在元素内部或外部凭空生成一个虚拟节点”比如在元素前面插入一段内容、画一条装饰线。我习惯这样记伪类像是一个“条件判断器”元素满足条件时样式生效伪元素像是一个“影子节点”它本身不在HTML里却能通过CSS生成内容。写法上也有讲究——按规范伪元素建议用双冒号::before、::after、::first-line、::first-letter伪类用单冒号:hover、:focus。老一点的ie8只认单冒号的伪元素写法不过现在基本不用管它了直接按规范用双冒号就行。还有一个区别值得注意伪类可以叠加使用比如a:hover:focus表示“鼠标悬停且获得焦点”的链接而伪元素通常一个元素只能插两个虚子节点::before和::after并且它们不会被nth-child这类结构化伪类计算在内。这一点看起来不起眼真到排查复杂选择器时会帮你省很多时间。1.2 伪类的五大分类状态、结构化、表单、逻辑与其他伪类数量不少但归纳起来大致是五大类记住这个框架后你看到陌生的伪类就不会慌先把问题归类再决定该查哪一类分类常见伪类典型使用场景状态类:hover、:active、:focus、:focus-within、:focus-visible、:link、:visited鼠标交互、键盘焦点、链接访问状态结构化:first-child、:last-child、:nth-child、:nth-of-type、:not、:empty、:only-child列表、表格、卡片布局的自动化样式表单类:checked、:disabled、:required、:valid、:invalid、:placeholder-shown、:in-range表单校验反馈、自定义单复选框逻辑类:is()、:where()、:not()、:has()选择器分组、父级判断、动态条件其他:root、:target、:lang()全局变量、锚点跳转样式、多语言适配别急着背下来。后面我会把每一类里真正高频的部分挑出来细讲其他冷门的用到时再查就行。以前端项目为例状态类和结构化类是出现频率最高的表单类在后台管理系统的表单页面里能发挥巨大作用逻辑类是近两年CSS升级后我最喜欢的新玩具尤其是:has()从“只能儿子找爹”变成了“爹也能找儿子”写某些布局是真的爽。2. 高频伪类逐个拆解从写法到细节2.1 状态类伪类的一线用法hover、active、focus 别再凭感觉写先看最常用的四个:hover、:active、:focus、:focus-within。很多开发者的认知停留在“hover是鼠标移入focus是点击输入框”这个层面但实际做项目时细节远比这多。:hover是鼠标悬停状态它不仅能用在链接上任何元素都可以加。我给按钮、卡片、表格行、下拉菜单都写过hover样式。但要注意移动端没有真正意义上的“鼠标悬停”手指点击时触摸浏览器通常会先触发一次hover如果点击后没有离开这个hover状态会一直留在元素上造成样式“卡住”的假象。我的处理方案是针对触摸设备写一层media (hover: none)把依赖hover的交互视觉关闭或者把这段逻辑交给JS的点击事件接管。:active表示元素被按下的那一小段时间。它最常见的应用是给按钮加按压缩放反馈比如.btn { transition: transform 0.1s ease; } .btn:active { transform: scale(0.96); }注意这个状态在键盘触发时不会出现所以如果组件要考虑无障碍访问不能只依赖:active做反馈。:focus是元素获得焦点时的状态。关键误区在于鼠标点击一个可聚焦元素时现代浏览器往往也会让它获得焦点并显示焦点环。对于追求视觉精致的产品这个默认的焦点环常常被设计师吐槽于是有些同学直接outline: none这就把键盘用户的导航指示器给砍掉了非常伤害无障碍体验。更好的做法是用:focus-visible它只在键盘输入Tab键移动焦点时显示焦点环鼠标点击时不会画额外边框这样两拨人都照顾到了。:focus-within是我特别推荐的“偷懒神器”它的意思是“元素自身或任意后代获得焦点时当前元素匹配”。你可以轻松实现一个整块搜索面板高亮的效果.search-panel:focus-within { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); }这样用户点进搜索面板里的输入框整块区域都会亮起来反馈感非常强而且不需要给每个输入框单独写focus样式。2.2 结构化选择器nth-child 与 nth-of-type 的相爱相杀结构化伪类的核心价值是减少对class的依赖让样式随着DOM结构自动变化。最常见的需求就是表格隔行变色、列表每个第几项特殊处理。这里最容易被绕进去的就是:nth-child和:nth-of-type的区别。一句话总结:nth-child是“在兄弟节点里数位置”数的时候不管你是div还是span只要是第几个就算第几个:nth-of-type是“在同类型标签里数位置”只有同标签的才参与计数。看个例子ul h2标题/h2 li项目1/li li项目2/li /ulli:nth-child(2)能选中“项目1”因为li是ul的第二个子元素。li:nth-child(1)选不中任何元素因为ul的第一个子元素是h2。li:nth-of-type(1)能选中“项目1”因为li元素同类里排第一。这个坑在渲染后端接口返回的列表时很常见比如列表里偶尔混入标题、分割线等非列表节点用:nth-child就会出现隔行变色错乱的问题。所以我在处理混合结构的列表时会优先考虑:nth-of-type或者给真正要计数的节点加个class。再说:nth-child的参数写法记住几个常用公式公式选中的元素odd或2n1序号为奇数的元素even或2n序号为偶数的元素3n1第1、4、7、10…个元素-n3前3个元素n2从第2个到末尾我之前做卡片瀑布流布局时需要给每行第一项去掉左边距用的就是-n3这种“前n个”写法。假设一行放3个卡片每个卡片有左边距那么第一行的第1、2、3个卡片不该有上边距可以用:nth-child(-n3)清掉。公式里的n从0开始计数代入几个值就能推出规律写的时候心里默算一遍比我每次都查文档靠谱得多。2.3 表单伪类让校验反馈不再依赖JS表单是伪类选择器最被低估的战场。以前做表单校验口味都要写一大段JS监听input事件现在原生CSS就能做很多基础校验视觉反馈。:required和:optional可以用来标记必填项和选填项。:valid和:invalid则是根据输入内容是否满足HTML校验规则自动生效规则来源包括required、pattern、minlength、maxlength、typeemail等。比如input:required { border-left: 3px solid #f59e0b; } input:valid { border-color: #10b981; } input:invalid { border-color: #ef4444; }听起来很美好但有一个体验陷阱页面刚加载时一个空的必填输入框在并没有“填过”的情况下也会被判定为无效于是红框立刻怼到用户脸上体验很差。解决办法是用:not(:placeholder-shown)或者:focus配合让校验样式在“不是空状态”时才显示input:not(:placeholder-shown):invalid { border-color: #ef4444; }这样只有当用户确实输入了内容且内容不合法时红色样式才出现。还有一个比较新的伪类:user-invalid它在用户完成输入交互后才判定无效语义更合理不过兼容性还在追赶中用之前记得查caniuse。:placeholder-shown是另一个宝藏它表示“当前占位符正在显示”也就是输入框是空的时候。反过来:not(:placeholder-shown)就表示“输入框里有内容”。这个特性做浮动标签效果非常顺滑——占位符还在时label和占位符重叠一旦用户输入了内容label就上移变小同时改变颜色.form-item input:placeholder-shown label { transform: translateY(0); font-size: 1rem; color: #9ca3af; } .form-item input:not(:placeholder-shown) label, .form-item input:focus label { transform: translateY(-20px); font-size: 0.75rem; color: #3b82f6; }不过要注意现代浏览器实现浮动标签更推荐用:placeholder-shown配合:focus因为部分浏览器不直接支持::placeholder的相邻选择器写法总是踩到各种奇怪边界。2.4 逻辑伪类is、where、not、has 的现代用法:is()和:where()都允许你把一串选择器合并成一个写法上几乎一样最大的区别是优先级。:is()的优先级是参数列表里优先级最高的那个而:where()的优先级永远是0。这个区别在团队协作覆盖样式时特别关键。举个例子。我有一段公共样式.sidebar :is(h1, .title, [data-tag]) { font-size: 16px; }:is()里含有类选择器.title所以整条规则优先级是 (0, 1, 0) 加父级部分后面想用一个简单类选择器覆盖它可能不够。如果我把:is换成:where.sidebar :where(h1, .title, [data-tag]) { font-size: 16px; }:where()优先级为0这条规则就只受.sidebar那部分影响后续任何类选择器都能轻松覆盖。所以我的习惯是写组件默认样式用:where()方便使用者覆盖写业务里不想被覆盖的兜底样式用:is()或明确类名。:not()的兼容性现在已经很好了它可以顺手排除某些元素。像我经常给列表项加分割线时会写成.list-item:not(:last-child) { border-bottom: 1px solid #e5e7eb; }这样就避免了最后一项多一条线的尴尬。注意老版本的:not()不能放复杂选择器比如不能写:not(.a .b)新规范已经放开但移动端某些低版本WebView可能不支持保守起见还是用简单的类名、标签名和基本伪类更稳妥。再来说说明星级父亲选择器:has()它让你终于能“往父级选”了。比如我想把“包含图片的卡片”加上内边距可以这样.card:has( img) { padding: 0; }比如实现“导航里当前鼠标悬停在某个链接时它的父级li高亮”可以配合:hover一起写li:has(a:hover) { background: #f3f4f6; }这个能力在复杂组件中作用很大但它最大的代价是性能。浏览器需要实时观察子元素状态来重新判定父级是否匹配如果全页面滥用重算成本会明显上升。我通常只在局部小范围元素上用比如导航、卡片组。3. 从速查到实战伪类选择器组合出常见UI效果3.1 导航菜单用hover和focus-within做下划线动画先从一个最常见的需求入手——导航菜单的鼠标移入事件也就是热搜词里反复出现的“css 鼠标移入事件”。在纯CSS里这就要靠:hover。我们希望鼠标移上去时文字下方有一条下划线从左往右展开而且键盘Tab聚焦到这个链接时也有同样的效果。先看HTML结构非常简单nav classnav a href#home首页/a a href#docs文档/a a href#about关于/a /nav核心思路是给链接加一个相对定位再用::after生成下划线默认transform: scaleX(0)藏起来当链接:hover或:focus-visible时缩放到1配合transform-origin控制展开方向.nav a { position: relative; padding: 8px 16px; text-decoration: none; color: #374151; } .nav a::after { content: ; position: absolute; left: 0; bottom: 0; width: 100%; height: 2px; background: #3b82f6; transform: scaleX(0); transform-origin: left center; transition: transform 0.25s ease; } .nav a:hover::after, .nav a:focus-visible::after { transform: scaleX(1); }这里有一处容易翻车的地方如果只给链接加:focus鼠标点击后焦点环和下划线会一直留在元素上视觉上很吵。用:focus-visible把鼠标点击的情况排除掉键盘用户能获得清晰的焦点定位鼠标用户也维持了干净的外观。如果想强化“焦点落在导航容器内”的整块反馈可以再给父容器写一个:focus-within比如让导航栏整体背景变浅.nav:focus-within { background: #f9fafb; }这样的导航菜单兼顾了鼠标和键盘两种操作模式在真实业务里已经算完成度很高的交互了。3.2 表格与列表结构化伪类的日常写法后台管理系统里表格是最常见的信息载体隔行变色、悬浮高亮、首行加粗这类需求几乎天天都有。用:nth-child和:hover配合能写出非常干净且不用改HTML的表格样式。先做斑马纹table classdata-table thead trth姓名/thth部门/thth状态/th/tr /thead tbody trtd张三/tdtd前端/tdtd在职/td/tr trtd李四/tdtd后端/tdtd在职/td/tr trtd王五/tdtd测试/tdtd休假/td/tr /tbody /table.data-table tbody tr:nth-child(even) { background: #f9fafb; } .data-table tbody tr:hover { background: #eff6ff; }注意一个细节斑马纹和悬浮高亮一起用时如果悬浮色和偶数行背景色太接近用户会分不清当前聚焦的是哪一行。我一般把悬浮色做得比斑马纹更深或者加一条左边框作为额外标识.data-table tbody tr:hover { background: #eff6ff; box-shadow: inset 3px 0 0 #3b82f6; }列表的分割线也能用结构化伪类处理上面提到过的:not(:last-child)是最常用的一招.list-item { padding: 12px 0; } .list-item:not(:last-child) { border-bottom: 1px solid #e5e7eb; }这样做的好处是后端增删列表项时你不需要去改第一项或最后项的特殊类名结构变了样式自动跟着变。如果列表里混入非列表节点别忘了用:nth-of-type代替:nth-child具体区别我在2.2节里已经说过。3.3 表单校验与浮动标签一张登录表单的完整样式接下来把表单伪类串起来做一个完整的登录表单顺带解决热搜词里“input居中”的问题。现代CSS做垂直水平居中用flex或者grid都非常简单我通常用flexdiv classlogin-wrapper form classlogin-form div classform-item input typeemail idemail placeholder请输入邮箱 required label foremail邮箱/label /div div classform-item input typepassword idpassword placeholder请输入密码 minlength6 required label forpassword密码/label /div button classlogin-btn typesubmit登录/button /form /div先让外层容器和表单居中.login-wrapper { display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #f3f4f6; } .login-form { width: 100%; max-width: 360px; padding: 32px; background: #fff; border-radius: 12px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05); }浮动标签的实现依赖输入框和label相邻当输入框有内容时label浮起来.form-item { position: relative; margin-bottom: 24px; } .form-item input { width: 100%; padding: 16px 12px 8px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 16px; outline: none; transition: border-color 0.2s, box-shadow 0.2s; } .form-item label { position: absolute; left: 12px; top: 12px; color: #9ca3af; pointer-events: none; transition: transform 0.2s, font-size 0.2s, color 0.2s; } .form-item input:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); } .form-item input:placeholder-shown label { transform: translateY(0); font-size: 16px; } .form-item input:not(:placeholder-shown) label, .form-item input:focus label { transform: translateY(-18px); font-size: 12px; color: #3b82f6; }这里的校验样式只会在用户输入内容后才触发避免刚打开表单就看到一片红.form-item input:not(:placeholder-shown):valid { border-color: #10b981; } .form-item input:not(:placeholder-shown):invalid { border-color: #ef4444; }注意typeemail天生自带格式校验邮箱填错时会自动匹配:invalid不需要额外写正则。密码框设了minlength6长度不足同样会被判为无效非常方便。3.4 优惠券锯齿与涟漪光圈伪元素配合伪类的小玩法热搜词里有“css 优惠券圆切”和“css 涟漪光圈扩散”这两个效果都和伪元素配合伪类有关我放在一起说。比如一个卡片在鼠标移入时中心区域向外扩散一圈光圈动画交互触发点是:hover光圈本体用::after生成.ripple-card { position: relative; overflow: hidden; width: 320px; height: 180px; background: linear-gradient(135deg, #6366f1, #8b5cf6); border-radius: 16px; } .ripple-card::after { content: ; position: absolute; left: 50%; top: 50%; width: 0; height: 0; background: rgba(255, 255, 255, 0.3); border-radius: 50%; transform: translate(-50%, -50%); opacity: 0; transition: none; } .ripple-card:hover::after { width: 300px; height: 300px; opacity: 1; animation: ripple 0.6s ease-out forwards; } keyframes ripple { from { width: 0; height: 0; opacity: 0.6; } to { width: 300px; height: 300px; opacity: 0; } }这个写法强调一个原则动画过程中尽量不要让浏览器频繁重排和重绘用width、height和opacity足够应付这类轻量动效。如果动画卡顿可以优先考虑用transform: scale()代替宽高动画性能会更稳。至于“优惠券圆切”通常是在优惠券左右两侧挖出半圆缺口单靠一个元素比较难同时做两侧常见方案是配合三个元素或者用遮罩和渐变。伪类在这里不是主角但它是触发交互的重要时机。我一般会把这类圆形锯齿效果留给遮罩方案做:hover负责控制缺口区域是否放大或变色整体组件看起来就比较生动了。4. 常见问题与排查技巧实录4.1 优先级与覆盖问题为什么写了不生效伪类选择器写了很多结果发现样式没生效这种情况我见得太多了。可能性很多但排在最前面的永远是优先级。要记住除:where()以外绝大多数伪类的优先级等同于一个类选择器也就是 (0, 1, 0)。比如a:hover的优先级比a高但比a.link-class低也比a#logo低得多。写了一个button:hover去覆盖另一个带class的按钮样式时如果对方写的是.primary-btn那两者的优先级都是 (0, 1, 0)后出现的那一条获胜。这时候通常会有人急着在button:hover前面加!important我的建议是别着急。先打开DevTools找到这个元素右下角会列出所有匹配的CSS规则逐条看哪个优先级更高、哪个排序靠后基本一眼就能定位。!important虽然能解决问题但会把后续维护者逼疯。还有一种小概率情况选择器写漏了。比如input:hover写成了input: hover多了一个空格冒号后面不能有空白写错就直接整条规则失效。我自己调试时会把选择器用引号包起来再检查一遍或者先给元素随便加个背景色确认选择器真的选得上再继续调整具体属性。4.2 兼容性速查哪些伪类可以放心用哪些要小心伪类选择器的浏览器兼容性差异比较大我这里整理一张常用伪类的速查表按“可以放心用”和“需要谨慎用”来分伪类兼容性使用建议:hover、:active、:focus所有浏览器放心用:link、:visited所有浏览器放心用但只有链接能匹配:focus-within现代浏览器可以放心用项目若需兼容ie11需加预案:focus-visible较新浏览器建议优先用但旧Edge/Safari老版本不支持:nth-child、:nth-of-type现代浏览器放心用:not()现代浏览器简单选择器放心用复杂选择器注意平台:checked、:disabled、:required现代浏览器放心用:valid、:invalid、:placeholder-shown较新浏览器移动端大多支持老WebView会有问题:is()、:where()现代浏览器放心用性能也不错:has()新浏览器谨慎用需降级方案我的经验是面向C端的项目尽量别依赖:has()这种新特性可以做渐进增强等浏览器支持了再播放更丰富的样式不支持也不影响核心功能。面向内部的工具系统浏览器版本可控:has()、:focus-visible这些新派伪类随便用开发效率是真的高。想确认具体版本支持情况打开caniuse网站搜索伪类名称即可。不要把“现代浏览器支持”等同于“用户浏览器支持”尤其是企业项目的用户环境千奇百怪提前查一下能避免很多线上事故。4.3 真实的坑我在项目中踩过的伪类选择器问题第一坑是:not()老语法的坑。我有一次给列表写:not(.active, .disabled)心想这是逻辑或的意思结果在某个老版本浏览器里整条规则作废列表样式直接崩了。后来改成了两段规则.list-item:not(.active):not(.disabled) { color: #374151; }这种写法在任何时代的CSS解析里都不容易出错属于保守首选。第二坑是触摸屏的:hover残影。移动端点击一个带hover样式的卡片后卡片经常保持着淡蓝色遮罩不消失直到点别处才恢复。我在一个营销活动页里遇到过用户反馈按钮点完像“被水渍粘住”。解决方法是把基于hover的视觉反馈限制在真正支持悬停的设备上media (hover: hover) { .card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1); } }这样触摸设备不会加载这些悬停样式残影直接消失。第三坑是:nth-child嵌套索引计数问题。比如一个循环渲染的评论列表外层有动态插入的广告位或加载提示这时:nth-child的序号会乱掉。我踩过最狠的一次是“每3条评论插入一条广告”结果广告也算进了计数导致所有斑马纹错位。后来我把规则的匹配范围从“所有子元素”缩小到“同类型的评论项”用:nth-of-type配合一个comment-item类才稳定下来。第四坑是:has()动态内容的性能问题。给整个列表容器写.list:has(.list-item.active)这种选择器时只要列表项增删浏览器就要重新计算匹配关系数据量稍微上来就会感受到明显的卡顿。我现在的原则是:has()只用来处理局部、静态、小范围的样式分支涉及大量数据渲染的地方老老实实在JS里加一个状态类名既明确又高效。最后分享一个我一直在用的调试技巧。DevTools的Elements面板里选中一个元素后右键菜单里有一个“Force State”的选项中文版叫“强制状态”可以直接强制元素处于:hover、:focus、:active这些状态。我写交互样式时从不用鼠标真的去悬停观察都是靠这个开关快速切换排查伪类没生效的情况特别顺手。养成这个习惯后你调试伪类选择器的速度会快上一大截。
返回列表