ARTICLE DETAIL

资讯详情

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

CSS伪类与伪元素:零JS实现悬停、动画与纯CSS开关

CSS伪类与伪元素:零JS实现悬停、动画与纯CSS开关 HTML写完了页面也能正常显示了但看起来总像一份打印出来的文档没点“活气”。这时候你缺的不是JavaScript而是CSS里的伪类和伪元素。作为前端入门绕不开的两个核心概念它们能让你不写一行脚本就做出按钮悬停、卡片翻转、消息角标、涟漪扩散、纯CSS开关这类“看着就像会前端”的效果。这篇文章我打算把伪类和伪元素从头到尾讲透包括它们到底是什么、怎么用、能落地在哪些场景以及一堆文档里不会写明的坑。特别适合刚学完HTML、正卡在CSS动效关口的萌新也适合准备前端面试时被问“伪类和伪元素的区别”就卡壳的同学。1. 伪类和伪元素到底是什么先搞懂概念再背选择器1.1 伪类元素的“状态表情”选择的是状态而不是元素本身很多萌新第一次看到:hover下意识以为它是某种class名在HTML里找来找去找不到于是整个人懵掉。实际上伪类根本不是新增一个DOM节点它只是一个“按状态或位置匹配”的选择器写法。:hover匹配的是鼠标悬停时的状态:focus匹配的是键盘输入焦点状态:first-child匹配的是父元素下的第一个子元素位置:not()则负责排除。我打过一个比方伪类像人的心情。人还是同一个人没有换身份证但开心时可以给他画个笑脸紧张时给他加两滴汗。CSS里的:hover就相当于说“当这个人心情是开心的时候给笑脸外观”。元素本身没有任何变化变的只是“当前状态”下的样子。能帮上忙的具体例子是表格隔行变色。以前没有结构性伪类的时候要么在HTML里手动给奇数行加class要么写JavaScript遍历现在一句搞定table tr:nth-child(odd) { background-color: #f7f8fa; }这里的odd等价于2n1也就是说从第一行开始每隔一行选中一次。想改成偶数行用even或2n就行。这个知识点几乎所有面试都会绕到至少要能说出nth-child(2n1)和:nth-of-type的一个区别nth-child只数位置不关心是什么标签而:nth-of-type会先按标签类型分组再数位置。举个例子p:nth-child(2)选择的是父元素的第二个子元素并且这个元素必须是pp:nth-of-type(2)选择的则是父元素里第二个p标签中间隔着别的标签不影响。1.2 伪元素通过CSS“凭空造物”文档里没有却真实渲染伪元素与伪类的本质区别在于它会向元素内部“插入”一个原本不存在的虚拟节点。最常见的::before和::after分别会在元素内容的最前面和最后面生成一个类似“子元素”的东西。它不在DOM树里但浏览器渲染的时候确实画出来了所以你在开发者工具里能看见在HTML源码里却找不到。注意冒号数量的变化。CSS2时代伪元素用单冒号比如:before和:after后来CSS3为了和伪类区分规范上统一改成双冒号::before、::after。为了兼容老IE单冒号写法至今仍被浏览器接受但新代码建议一律写双冒号。面试被问“为什么有的伪元素写一个冒号有的写两个”能答出这是CSS2到CSS3的历史遗留说明你是真懂。伪元素最常见的一个玩法是给标题加装饰竖条.block-title::before { content: ; display: inline-block; width: 6px; height: 18px; background: #ff6b35; margin-right: 8px; }这样每个标题前面会自动出现一条橙色竖线HTML里完全不用多写span标签。为什么要强调这点因为减少真实DOM节点意味着结构更干净改样式时只需要动CSS不需要去HTML里找标签改类名维护成本低很多。1.3 三秒判断小技巧数冒号和判断是否产生新节点很多新手分不清某个选择器是伪类还是伪元素我提供一个最实用的判断套路数冒号然后想它到底是“选状态”还是“造节点”。类型常见写法典型例子作用伪类单冒号:hover:focus:first-child:checked按状态、位置、关系选元素伪元素双冒号::before::after::first-line::selection插入新节点或修改局部:hover是伪类因为只是选中悬停状态下的元素::before是伪元素因为它真的生成了一个虚拟节点。顺带提两个不那么常被提的伪元素::first-letter可以做首字下沉::first-line让第一行文字变特殊样式写文章详情页的时候很好用.article::first-letter { font-size: 40px; float: left; font-weight: bold; margin-right: 6px; }整段文字首字立刻放大并靠左浮动形成杂志里的首字下沉效果不用额外包标签。2. 交互态实战把hover、focus、checked用到真实场景2.1 hover鼠标悬停的反哺最容易上手的第一个动效:hover是前端入门必须熟练掌握的第一个状态但很少有人认真想过“为什么需要配合transition”。没有过渡的情况下鼠标一移上去样式“咔”地一下硬切视觉效果像换了个按钮而不是按钮在动。加上transition之后颜色、位移、阴影都会在指定时长内平滑过渡用户会感觉到“这东西是活的”。一个完整的按钮悬浮效果.btn { background: #1677ff; color: #fff; padding: 10px 24px; border: none; border-radius: 6px; transition: all 0.3s ease; } .btn:hover, .btn:focus-visible { background: #0958d9; box-shadow: 0 6px 16px rgba(22, 119, 255, 0.35); transform: translateY(-2px); }transform: translateY(-2px)让按钮向上平移2像素配合阴影看起来像“浮起来”。把:focus-visible也写进去是因为键盘用户按Tab键切到按钮时同样需要一个视觉反馈这才是完整的可访问性习惯。这里有一个移动端大坑手机上并没有真正意义上的“鼠标悬停”手指点一下会触发:hover手指移开后有些浏览器会一直保持这个状态直到点击别处。如果你做的悬浮效果是“卡片上浮”手机上就容易出现点一下之后卡片一直浮着的怪相。稳妥做法是用媒体查询把悬停效果限定在支持悬停的设备上media (hover: hover) { .card:hover { transform: translateY(-4px); } }这条规则的意思是“只有在支持悬停的设备上才应用:hover样式”不带这个保护的话很多团队只测桌面端就上线结果移动端一堆灵异表现。2.2 focus与focus-within表单体验和键盘可达性的关键:focus是一个元素能够接受键盘输入时触发的状态最常见的场景是input被点击时。很多前端为了美观直接写outline: none这是典型的可访问性事故键盘用户按Tab键切到输入框时没有任何可见提示根本不知道焦点在哪。但默认的outline又确实不好看所以推荐用:focus-visibleinput:focus-visible { outline: 2px solid #ff6b35; outline-offset: 2px; }:focus-visible只在键盘导航这类“非鼠标点击”的情况下显示焦点圈鼠标点击输入框时不会出现难看的默认光圈。优先级上它比:focus更高一些所以可以放心写。:focus-within更进阶它匹配的是“当前元素自身或者它的任意后代元素获得焦点”的状态。典型场景是整个表单卡片高亮当输入框聚焦时希望包含输入框的整个卡片边框亮起来。.form-item:focus-within { border-color: #1677ff; box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.2); }点击输入框后包着它的.form-item立刻出现蓝色光晕不需要JavaScript去监听focus事件。这个特性在很多组件库的表单组件里被大量使用也是面试中容易考到的一个点因为“用CSS解决本来自以为要写JS的问题”是面试官最喜欢的加分表现。2.3 checked和disabled不写一行JS也能做开关:checked是表单控件的选中状态配合label和兄弟选择器可以做出一个纯CSS的开关组件。原理不复杂label里包裹着看不见的checkbox点击label会切换checkbox的选中状态CSS再根据选中状态改变后面元素的外观。label classswitch input typecheckbox classswitch-input span classslider/span /label.switch-input { position: absolute; opacity: 0; width: 0; height: 0; } .slider { width: 50px; height: 28px; border-radius: 14px; background: #ccc; position: relative; display: inline-block; transition: background 0.3s; } .slider::after { content: ; position: absolute; left: 3px; top: 3px; width: 22px; height: 22px; border-radius: 50%; background: white; transition: left 0.3s; } .switch-input:checked .slider { background: #1677ff; } .switch-input:checked .slider::after { left: 25px; }这里同时用到了:checked伪类和::after伪元素两个知识点合在一起效果非常直观。圆点在左侧背景灰色选中后圆点滑到右侧背景变蓝。注意几个细节checkbox不能从DOM里移除只是用opacity: 0加绝对定位把它藏起来这样点击label时依然能切换状态读屏器也能识别到它是一个开关不破坏可访问性。:disabled也经常被忽略。给按钮或输入框加上禁用样式时除了改颜色还要注意视觉对比度和清楚表达“不可点击”的状态input:disabled { background: #f5f5f5; color: #999; cursor: not-allowed; }cursor: not-allowed只是“箭头变化”这一层真正防呆还是要靠disabled属性本身让元素无法交互。3. 伪元素的正确打开方式从装饰小物件到清浮动3.1 content属性伪元素的第一条铁律为什么很多人写::before、::after页面上一丁点反应都没有十有八九是忘了写content。规范要求伪元素的内容生成依赖content属性哪怕你只想画一个纯色背景也必须先写一个空字符串content: 。不写这一行伪元素连“被渲染”的资格都没有。.icon::before { /* content: ; 忘了这一行整个样式都失效 */ width: 10px; height: 10px; background: red; }上面这段如果去掉注释里的content宽高、背景全部不会生效。我见过不少新人在排错时反复检查宽高和定位最后才发现是content没写。所以先养成习惯凡是使用伪元素第一行永远是content。content不只是空字符串它还能读元素的属性值。利用attr()函数可以做一个完全不需要JS的提示气泡button classtip>.tip { position: relative; } .tip::after { content: attr(data-tip); position: absolute; bottom: calc(100% 8px); left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.8); color: #fff; padding: 4px 10px; font-size: 12px; white-space: nowrap; border-radius: 4px; opacity: 0; pointer-events: none; transition: opacity 0.2s; } .tip:hover::after { opacity: 1; }鼠标悬停时>.clearfix::after { content: ; display: block; clear: both; height: 0; visibility: hidden; }逐行拆解一下content: 保证伪元素存在display: block让伪元素作为块级元素占据一行clear: both清除左右两侧的浮动影响父容器在计算高度时会把伪元素也算进去height: 0和visibility: hidden确保这个伪元素不会产生可见的空隙。有人问能不能直接用overflow: hidden效果上确实能让父级包住浮动子元素但代价是有可能裁剪掉你想让内容溢出的部分比如下拉菜单、气泡提示所以clearfix是更安全的通用方案。现在新的项目基本都用flex或grid布局很少遇到浮动塌陷。但旧代码里到处是.clearfix面试官也喜欢问原理理解这几行参数的意义比会背模板重要得多。3.3 伪元素画图形三角形、角标、分隔线用伪元素画小图形是性价比极高的能力。三角形可以用border来画元素宽高为0给边框设一个厚度把其中三边颜色设为透明剩下一边就是三角形。.triangle::after { content: ; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 8px solid #333; display: inline-block; margin-left: 4px; }这段生成一个向下的小三角常用来做下拉菜单的指示箭头。想换方向就调整哪边有色、哪边透明。消息未读数红点角标也是伪元素的拿手好戏。假设头像一个按钮外部包一个相对定位的容器角标由::after生成.badge { position: relative; } .badge::after { content: attr(data-count); position: absolute; top: -8px; right: -8px; min-width: 18px; height: 18px; line-height: 18px; background: #f5222d; color: #fff; font-size: 12px; border-radius: 9px; text-align: center; padding: 0 4px; box-sizing: border-box; }注意min-widthtext-align: centerbox-sizing: border-box的组合让角标在数字只有一位时接近圆形两位数时自动拉宽不会挤爆布局。伪元素的数量有限制单个元素只有::before和::after两个坑位如果一个角落要放三种角标就得考虑增加包裹层或改用真实DOM。3.4 替换元素上的伪元素这是个容易被坑的考点有一个高频坑给img、svg、iframe这类替换元素写::before或::after经常不生效。原因在于替换元素的内容是由外部资源“替换”进来的比如img的图片内容本身就占了整个元素内部没有一个明确的“内容前部”或“内容后部”插入点可以插虚拟节点。虽然不同浏览器表现不完全一致但规范层面并没有定义替换元素的伪元素行为实用建议是不要把伪元素直接挂在img上。解决办法很简单把img包在一个div里在div上使用伪元素div classavatar-wrapper img srcuser.png alt用户头像 /div.avatar-wrapper { position: relative; } .avatar-wrapper::after { content: ; position: absolute; right: 2px; bottom: 2px; width: 10px; height: 10px; background: #52c41a; border-radius: 50%; border: 2px solid #fff; }这是一个“在线状态绿点”的常见实现绿色小圆点在头像右下角表示用户当前在线。4. 让伪类伪元素真正“动”起来transition与animation4.1 transition状态改变要“顺滑”别硬切transition是最简单也最常用的动效方案它监听样式从旧值变为新值的过程在两个值之间做补间动画。:hover把背景色从蓝色变成深蓝transition负责让颜色在0.3秒内渐变过去。没有它颜色就是瞬间切换用户看到的是“跳变”而不是“变化”。语法拆开看是transition: property duration easing delay通常写成.item { opacity: 1; transition: opacity 0.3s ease; } .item:hover { opacity: 0.6; }一个容易踩的大坑transition对display无效。display不是一个可插值的属性不存在从none平滑到block的中间状态所以display: none和display: block之间的切换永远是瞬变。要实现元素的淡入淡出标准组合是visibilityopacity.tip { visibility: hidden; opacity: 0; transition: opacity 0.3s, visibility 0.3s; } .parent:hover .tip { visibility: visible; opacity: 1; }visibility虽然也是离散属性但它有个特性当值从visible变为hidden时可以等opacity的过渡结束再隐藏反过来hidden变visible时立即生效。于是这种写法既能淡入淡出又不会让隐藏元素阻挡点击。这就是为什么很多组件库的悬浮层都不直接用display: none而是绕一圈用这套组合。4.2 animation与keyframes不依赖状态也能持续动:hover这种状态触发只能服务“鼠标进来”那一刻想做一个自动循环的涟漪扩散、加载动画、扫光效果就要靠animation加keyframes。涟漪扩散是个特别典型的伪元素动画场景keyframes ripple { 0% { transform: scale(1); opacity: 0.6; } 100% { transform: scale(2); opacity: 0; } } .ripple { position: relative; } .ripple::after { content: ; position: absolute; inset: 0; border-radius: 50%; background: rgba(22, 119, 255, 0.3); animation: ripple 1.6s infinite; }扩散层是::after生成的圆从原始大小逐渐放大到2倍同时透明度降到0循环播放看起来就像水面波纹一圈一圈荡开。这里伪元素的优势又体现出来动画效果不需要在HTML里单独放一个div样式和结构完全分离。按钮扫光也是常见玩法。给按钮的::after做一条半透明白色斜条用关键帧动画让它从左侧扫到右侧.btn { position: relative; overflow: hidden; } .btn::after { content: ; position: absolute; left: -50%; top: 0; width: 50%; height: 100%; background: linear-gradient(120deg, transparent, rgba(255, 255, 255, 0.5), transparent); animation: shine 1.5s infinite; } keyframes shine { 0% { left: -50%; } 100% { left: 120%; } }注意父级加了overflow: hidden否则扫光条会跑到按钮外面。这种细节如果不写动画就会穿帮做组件库时尤其要小心。4.3 懂一点3D旋转perspective、preserve-3d和旋转轴热词里常能看到transform: rotateY(60deg) translateZ(300px)这样的写法这属于CSS 3D变换的范畴。3D效果很多人学得云里雾里其实核心就两个父级要设perspective来提供“近大远小”的透视感元素自身有时要设transform-style: preserve-3d让子元素的3D变换不被拍平。一个最常用的卡片翻转效果.card-flip { perspective: 800px; } .card-inner { transition: transform 0.6s; transform-style: preserve-3d; } .card-flip:hover .card-inner { transform: rotateY(180deg); }perspective: 800px是视距800像素相当于观众离物体大约800像素远值越小透视变形越强烈。rotateY(180deg)让卡片绕Y轴水平翻转180度看起来像翻到了背面。三个轴向的判断口诀我自己的经验是——rotateX是上下翻转像小人翻跟头rotateY是左右翻转像翻扑克牌rotateZ是平面内转圈。记不清方向的时候直接在浏览器里改数值看实时效果是最快的。如果以后想做一个3D环绕相册思路就是这个方向父级perspective加transform-style: preserve-3d每个子项用rotateY(60deg) translateZ(300px)沿着圆周摆放同时整个容器用animation驱动旋转。5. 面试与避坑速查这些坑我全踩过5.1 伪类和伪元素的区别一句话回答法面试现场被问到“伪类和伪元素的区别”最稳的回答结构是伪类代表元素的一种状态或位置关系比如:hover、:focus、:first-child它不产生新节点伪元素则是创建了一个文档树中原本不存在的虚拟节点比如::before、::after、::first-line。从写法上区分伪类是一个冒号CSS3之后建议伪元素统一写两个冒号。这样说清楚了两者的概念差异也顺带把历史规范答了面试官会认为你是理解而不是背的。再补一句加深印象的对比伪类负责“在什么状态下改样式”伪元素负责“凭空造出什么东西来改样式”。一个是给元素换衣服一个是往元素身上加配饰。5.2 选择器权重为什么hover样式会被别的规则盖住CSS优先级是萌新最容易翻车的地方。权重计算规则粗略说行内样式 ID选择器 类/属性/伪类选择器 类型/伪元素选择器。:hover属于伪类和普通类的权重在同一级。一旦遇到“hover没有反应”多半不是没写对而是被权重更高的规则压住了。举个经典例子.nav .link { color: #333; } .link:hover { color: #1677ff; }第一行的权重是“两个类”第二行的权重是“一个类加一个伪类”伪类虽然参与计算但整体少了一个量级所以第一行赢:hover里的颜色根本不会显示。新人看到的结果就是“鼠标移上去颜色不变”。修复方式有两种把第一个选择器写成.nav .link:hover提升权重或者直接写成.nav .link: hover不用担心内聚性问题也可以改成a.link:hover类型选择器虽然参与计算但不会增加量级至少让第二行的伪类位置反超。我不建议用!important解决这种问题它会把整个项目的优先级体系打乱后续任何人改样式都寸步难行。5.3 高频踩坑速查表我把这几年见得最多的伪类伪元素报错现场整理成一张表可以直接存下来当备忘现象原因修复方案::before/::after完全不显示没写content补上content: 双冒号在老浏览器不显示旧IE只认单冒号老项目用:before兼容给img加伪元素没效果替换元素不支持伪元素外包一层div再挂伪元素移动端hover样式“卡住”触摸触发:hover后不消失用media (hover: hover)限定写好的hover样式不生效权重被更高规则覆盖提升选择器权重动画里的透明度/位移没变化漏了transition或写错了属性检查要过渡的属性和初始值transition对display无效display不可插值改用opacityvisibility伪元素盖住了按钮文案且按钮无法点击伪元素拦截了鼠标事件加pointer-events: none最后一行值得展开说。::after如果覆盖在按钮上方它可能会拦截点击事件导致你点了按钮却没有任何反应。给装饰性伪元素统一加一句pointer-events: none既不影响视觉效果也不会挡操作是一个成本极低收益稳定的好习惯。5.4 用DevTools调试伪类和伪元素Chrome开发者工具是学习伪类伪元素的最好工具没有之一。选中一个元素后右侧Styles面板里有一个:hov按钮点开就能强制开启hover、focus、active等状态不需要移动鼠标就能预览各种交互态样式。这对调试那种“鼠标一移走状态就消失”的效果特别有用。伪元素在Elements面板里显示为所选元素下面的::before、::after伪节点可以直接点击查看它们的样式和content内容。想临时验证效果直接在面板里改数值就行改完再拷贝回编辑器。这种调试方式能帮你快速确认一条规则到底有没有匹配上。6. 实操心得与两个提升效率的小习惯主要知识点讲完了我再分享几个自己一直在用的工作习惯都是从实际项目里沉淀出来的。第一个习惯是“能用伪元素解决的就不加标签”。做分割线、小箭头、红点角标、tooltip气泡、背景装饰我第一反应都是先想::before或::after能不能完成。这样做的收益很实在HTML结构清爽未来改样式不用动结构层代码review时也容易看懂。当然伪元素只有两个坑位遇到复杂结构还是要老实加真实节点核心标准是“适度”。第二个习惯是交互无响应时先查CSS再怀疑JS。我见过太多新人在hover没反应时第一反应是去写JavaScript绕了一大圈才发现只是权重问题。正确的排查顺序是打开控制台确认元素确实匹配到了那条规则再看这条规则有没有被更高权重的覆盖最后才考虑脚本。八成的问题出在选择器本身养成这个习惯能省很多时间。第三个小心得利用:checked加伪元素组合画“自定义对勾”。系统原生的checkbox在不同平台长得乱七八糟用伪元素画对勾再配合:checked切换显示就能在所有平台保持一致。画对勾可以这样写.checkbox { width: 18px; height: 18px; border: 2px solid #ccc; border-radius: 4px; position: relative; } .checkbox::after { content: ; position: absolute; left: 5px; top: 1px; width: 4px; height: 9px; border-right: 2px solid white; border-bottom: 2px solid white; transform: rotate(45deg); opacity: 0; } .checkbox.checked::after { opacity: 1; }伪元素画对勾的核心是用旋转后的两条边线拼出勾形rotate(45deg)是这里面的关键。类似的思路可以画关闭按钮的叉、下拉箭头等等动画库的底子就是这么一点点积出来的。最后补充一个我写卡片悬停效果的常年标配指定过渡属性不用transition: all。写成transition: transform 0.3s ease, box-shadow 0.3s ease能避免很多莫名其妙的问题比如元素本身还有别的变换动画时被:hover的all过渡误伤性能上也比监听所有可插值属性更好。这个写法我在项目里用了好几年几乎没有出过问题推荐你也试试。
返回列表