CSS 里有两类概念看起来很像用起来也经常混在一起一类叫伪类一类叫伪元素。再加上手滑把::before打成:befor一条规则就这么静默失效半天找不到原因。这篇就把最近被问得最多的一组问题整理清楚::before到底怎么用:befor到底是不是个东西伪类和伪元素又差在哪。这个内容适合两类人刚把 HTML 和 CSS 笔记翻完、准备做第一个交互动效的新手以及已经写了一阵子样式但被旧项目里的:before、:after兼容写法弄得头疼的维护者。理解了底层差异之后写这种选择器基本就是肌肉记忆不会再瞎猜。1. 伪类和伪元素先别急着背语法很多教程一上来就说“伪类是单冒号伪元素是双冒号”这个说法对但如果你不理解它们各自解决什么问题很快又会把:nth-child和::first-letter混在一起。1.1 伪类是条件筛选器伪类解决的是“怎么在一堆已有元素里挑出符合条件的元素”。比如:hover表示鼠标悬停的瞬间:focus表示元素获得焦点:first-child表示它是父元素的第一个子元素。这些元素本身早就存在于 HTML 结构里伪类只是给选择器加了一个“状态条件”或者“位置条件”并不产生任何新的页面节点。你可以把伪类理解成点名屋子里已经坐了二十个人你现在要选出“戴帽子的人”“坐在第一排的人”“正在举手的人”人没有变变的是筛选规则。1.2 伪元素是虚拟盒子伪元素恰恰相反它解决的是“在页面里造一个本来不存在的东西”。::before会在当前元素的内容盒子内部、所有真实内容的最前面凭空生成一个虚拟子元素。这个虚拟子元素不是真实标签不会出现在 HTML 结构里也不受 JavaScript 直接操作但在视觉和布局上它真的占一个位置能设置宽高、背景、边框、动画。可以把它理解成“套娃”父元素是一个盒子::before就是在盒子里面再塞一个小盒子::after则是塞在最后面。1.3 一张表看穿区别维度伪类伪元素本质对已有元素的筛选条件虚拟生成的元素盒子语法单冒号如:hover规范推荐双冒号如::before是否创建新节点不创建创建虚拟子节点content 属性不需要::before/::after必须有典型例子:hover、:focus、:first-child、:checked::before、::after、::first-letter、::placeholder作用对象元素本身或元素的状态元素内部生成的内容把这张表记牢后面所有代码都不会跑偏。2. 一个字母引发的静默失效::before 与 :befor2.1 ::before 到底在做什么::before的作用是“在当前元素内容的前面插入一个虚拟子元素”。一个最简单的例子.badge::before { content: 热卖; color: #f56c6c; }HTML 结构里只需要写span classbadge商品A/span渲染出来时页面上会显示成“热卖商品A”。这个“热卖”并不是写在 HTML 里的而是浏览器根据::before规则生成的。这里有个强规则content不能省略。你可以写成content: 留一个空字符串但不能不写。如果content属性缺失或者值是none这个伪元素根本不会被渲染。很多人第一次写伪元素不显示十有八九就是忘了加content。2.2 :before 不是错误:befor 才是先说结论::before是现代 CSS 的标准写法推荐使用。:before是旧规范的单冒号写法功能与::before完全一致现代浏览器为了兼容老网站也一直保留支持。:befor是拼写错误它不是“旧的 ::before”也不是“另一种伪元素”它是完全无效的选择器。为什么会有人写:befor因为它和before长得太像漏掉最后一个e手一快就出去了。更重要的是浏览器不会为这种非法选择器弹错误提示。CSS 解析器碰到无法识别的规则通常就是整条规则静默丢掉既不报错也不影响其他规则。于是表现就是“这条样式写没写都一样”查起来非常痛苦。2.3 正确写法和常见误拼写对照写法是否有效含义::before有效标准的伪元素写法现代项目统一使用:before有效旧规范写法兼容 IE8功能等同于::before:befor无效拼写错误规则被忽略::befor无效拼写错误规则被忽略:befroe无效手误规则被忽略还有一个特别坑的细节如果一条选择器列表里混入了非法选择器整条规则都可能被丢弃。/* 这段代码不会生效 */ .badge::before, .badge:befor { color: red; }第二个选择器非法会让浏览器认为整条规则无效。所以写完样式之后我习惯顺手搜索一下befor、afterr、before:这类常见错误拼写尤其是从别人那里复制过来的样式。3. 把 ::before / ::after 用到一个水平核心细节3.1 content 是灵魂::before和::after的content不只是能写死字符串它有几种常见用法.tooltip::after { content: attr(data-hint); } .icon-star::before { content: url(icons/star.svg); } h2::before { counter-increment: chapter; content: 第 counter(chapter) 章 ; }content: attr(data-hint)可以读取当前元素的>.way1::before { content: ; display: block; width: 40px; height: 40px; } .way2::before { content: ; display: inline-block; width: 40px; height: 40px; } .way3::before { content: ; position: absolute; left: 0; top: 0; width: 40px; height: 40px; }第三种写法里绝对定位会把元素自动按块级盒处理所以即使不显式写display: block宽高也能生效。注意如果要用position: absolute最好让父元素有position: relative。定位参照物一旦跑偏伪元素会飞到页面奇怪的位置。3.3 不是所有元素都能挂::before和::after在大部分普通盒元素上都能用包括div、span、button、a、p等。但替换元素是个例外比如img、input、video、textarea这类元素在绝大多数浏览器里给它们挂::before、::after是不可靠的。我见过很多同学在input上写::after想做搜索图标折腾半天不显示其实是方向错了。正确做法是在input外面包一层div图标挂在包裹层上。3.4 定位与优先级伪元素也是参与 CSS 优先级计算的。在0,0,0三位一组里伪元素按元素位计算.foo的选择器优先级是0,1,0.foo::before是0,1,1.foo:hover::before是0,2,1:hover这类伪类按类选择器计算所以优先级比普通标签选择器高。理解这个之后你就知道为什么有时候两条规则都作用在伪元素上却只有一条生效。4. 伪类家族盘点除了 hover你还要知道这些4.1 状态伪类所谓的“鼠标移入事件”CSS 里没有“鼠标移入事件”这种说法hover是一个状态伪类它匹配“鼠标正在悬停”的那一刻。.btn:hover { background: #3a66ff; }常见状态伪类还有:active元素被按下的瞬间:focus元素获得焦点键盘 Tab 也能触发:focus-within元素自身或它的后代获得焦点时触发:visited链接访问过后:target当前锚点指向的元素:focus-within特别适合做表单卡片的高亮效果只要输入框获得焦点整个卡片都会亮起来不需要 JavaScript。4.2 结构伪类的两个高频坑结构伪类最容易被误解的是:first-child和:first-of-type。div classlist p说明文字/p span第一个 span/span span第二个 span/span /div如果写.list span:first-child { color: red; }这个规则不会生效。因为.list的第一个子元素是p不是span。span:first-child要求这个 span 自己必须是父元素的第一个子元素但它的前面还有一个p。如果用.list span:first-of-type { color: red; }就会生效。:first-of-type的意思是“在所有兄弟span里选第一个”它不要求这个元素是整个父级的第一个子元素。再看:nth-child(anb)很多人只会背odd和even其实公式很有用:nth-child(3n1)选第 1、4、7、10……个:nth-child(-n3)选前 3 个:nth-child(2n1)等于:nth-child(odd)选奇数个4.3 表单伪类让校验不发愁表单状态伪类在做“无 JavaScript 校验”时特别好用input:required { border-color: #ccc; } input:valid { border-color: #67c23a; } input:invalid { border-color: #f56c6c; } input:disabled { opacity: 0.6; }:checked可以配合自定义复选框.checkbox:checked label::after { content: ; display: inline-block; width: 16px; height: 16px; background: #4f7cff; border-radius: 4px; }这里:checked是伪类负责判断选中状态 label::after里的::after是伪元素负责生成一个可视的对勾背景。两者配合形成一个完整的效果。4.4 伪类和伪元素合体伪类和伪元素不是对立关系它们经常一起出现在选择器里。.btn::before { content: ; position: absolute; left: 0; right: 0; top: 0; bottom: 0; border: 2px solid #4f7cff; border-radius: 8px; opacity: 0; transition: opacity 0.2s ease; } .btn:hover::before { opacity: 1; }这里的逻辑是伪元素负责“画一个边框圈”伪类:hover负责“鼠标悬停时让这个圈显示出来”。触发条件写在父元素名后面写法是.btn:hover::before不要倒过来写.btn::before:hover。状态应该依附在真实元素上让它的伪元素做出响应这种写法最稳。5. 实操一个涟漪光圈按钮的完整实现前面讲了原理现在做一个能直接复制跑的完整小案例。场景就是一个按钮鼠标移入时出现一个光圈同时有一个涟漪从中心向外扩散。5.1 页面骨架和样式引入方式先建一个演示页固定舞台尺寸为1440px × 810px方便对照设计稿预览!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title涟漪光圈按钮/title link relstylesheet hrefdemo.css /head body main classstage div classcard div classprice span classdel199/span span classnow99/span /div button classbtn ripple span classbtn-text立即参与/span /button /div /main /body /html这里用的是外部 CSS 引入方式HTML 里写link relstylesheet hrefdemo.css样式全部放在demo.css文件中。很多新手会把style标签一起复制进.css文件这是错的。.css文件里只能写选择器和声明块style是 HTML 文件的标签不是 CSS 的语法。另一个不推荐的方式是import它要在样式表解析时才发起下载等于串行加载会影响首屏性能。除非特殊场景平时用link就够了。demo.css基础部分html { font-size: 16px; } .stage { width: 1440px; height: 810px; display: flex; align-items: center; justify-content: center; margin: 0 auto; background: #f6f8ff; } .card { width: 360px; padding: 2.5rem; text-align: center; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); } .btn { position: relative; display: flex; align-items: center; justify-content: center; min-width: 180px; height: 56px; padding: 0 24px; border: none; border-radius: 8px; background: #4f7cff; color: #fff; font-size: 1rem; cursor: pointer; overflow: hidden; }按钮文本的垂直和水平居中在这里用display: flex加align-items、justify-content实现。比用line-height硬凑要稳尤其当按钮里有图标和文字混排时line-height方案容易出偏差。5.2 用 ::before 做常驻光圈常驻光圈可以理解为一个比按钮略大的描边层平时隐藏鼠标移入时淡入.btn::before { content: ; position: absolute; top: -6px; right: -6px; bottom: -6px; left: -6px; border-radius: 12px; border: 2px solid rgba(79, 124, 255, 0.4); opacity: 0; transition: opacity 0.25s ease; pointer-events: none; } .btn:hover::before { opacity: 1; }这里给::before加了一行pointer-events: none非常重要。否则这个虚拟盒子虽然透明但会盖在按钮上方鼠标点击时可能被它拦截导致按钮上的点击事件失效。5.3 用 ::after 做扩散涟漪涟漪效果可以用::after画一个圆鼠标悬停时触发一次放大动画.btn::after { content: ; position: absolute; left: 50%; top: 50%; width: 10px; height: 10px; border-radius: 50%; background: rgba(255, 255, 255, 0.5); transform: translate(-50%, -50%) scale(0); opacity: 0; pointer-events: none; } .btn:hover::after { animation: ripple 0.6s ease-out; } keyframes ripple { 0% { transform: translate(-50%, -50%) scale(0); opacity: 0.6; } 80% { opacity: 0.2; } 100% { transform: translate(-50%, -50%) scale(12); opacity: 0; } }这个效果就是常说的“CSS 涟漪光圈扩散”。核心原理并不复杂一个小圆点从按钮中心开始用scale放大到覆盖按钮透明度从0.6降到0看起来就像水波扩散。如果用户系统开启了“减少动态效果”偏好最好加一条降级处理media (prefers-reduced-motion: reduce) { .btn::before, .btn::after { animation: none; transition: none; } }这是很多项目容易忽略的可访问性细节但加上之后动效敏感用户会舒服很多。5.4 图片和文字一行、字体渐变、删除线和单位换算按钮里的文本和图标要排成一行我常用inline-flex.btn-text { display: inline-flex; align-items: center; gap: 0.5rem; } .btn-text::before { content: ▶; font-size: 0.75rem; }原理和“图片和文字一行”完全一样把文本容器变成flex子项通过align-items: center垂直居中有缝隙用gap控制。比手动写margin-right干净。删除线效果也不用额外加span用伪元素画一条线就行.del { position: relative; color: #999; } .del::after { content: ; position: absolute; left: -0.1em; right: -0.1em; top: 50%; border-top: 1px solid #f56c6c; }字体渐变可以用background-clip: text对按钮标题也很适合.gradient-text { font-size: 2rem; font-weight: 700; background: linear-gradient(90deg, #a1c4fd, #c2e9fb); -webkit-background-clip: text; background-clip: text; color: transparent; }至于单位这里把常见几个一起说清单位基准典型用途备注pxCSS 像素边框、小尺寸修饰固定尺寸桌面端最直观em当前元素字号按钮内边距、相对字号的装饰嵌套时容易级联变大rem根元素字号间距、标题字号根字号 16px 时1.5rem就是 24pxrpx小程序专用单位微信小程序布局不要用在桌面端网页里桌面端如果拿到 1440px 宽的固定设计稿最稳妥的方式是外层容器用 px 或 rem 结合内部间距用 rem 保持比例。强行把所有小屏适配都压在长度单位上不如配合clamp()和媒体查询一起处理。6. 常见问题排查与避坑速查表6.1 伪元素没显示按这个顺序查我排查这类问题基本固定一个顺序命中率接近九成检查content有没有写。没有content伪元素不会渲染。检查拼写。:befor、:befroe这类都是无效选择器。检查.css文件里有没有误带style标签。如果有整份文件都可能失效。检查引入方式。确认 HTML 里用的是link relstylesheet路径写对没有。检查元素类型。如果是img、input这类替换元素直接考虑换方案在它的包裹层上挂伪元素。检查盒模型。如果设了width、height但没显示八成是display还是inline补一个display: block或inline-block。检查z-index和叠放。如果伪元素被其他内容盖住调整z-index并确认父元素创建了定位上下文。6.2 选择器规则看着没问题但结果不对典型的例子是:first-child和:first-of-type混用。前面的例子已经说过:first-child要求元素必须是父级的第一个子元素而:first-of-type只要求在同类兄弟里排第一。还有一种常见情况是在样式里混写了单双冒号:before { content: 前缀; }单冒号的:before在旧项目里能跑但新代码里建议统一写成::before。遇到老代码保留原样即可不要为了“统一”批量替换因为 IE8 只认单冒号写法如果项目还要兼容旧环境:before反而更稳。6.3 伪元素的交互和可访问性伪元素虽然看起来是一个盒子但它不算是真实的 DOM 节点。JavaScript 里直接querySelectorAll(::before)是拿不到它的绑定事件更不可能。想监听伪元素的点击实际上监听的是它的父元素。这里还要注意可访问性content里的文本内容有的读屏软件会把它读出来。纯装饰性的内容比如一个箭头、一个圆点最好用空content 背景图或背景色来实现而不是把无意义的符号写进content。所有关键文案都应该放在真实的 HTML 结构里。我个人在实际排查中的习惯是先打开开发者工具的 Styles 面板看那条伪元素规则有没有被画删除线。如果规则在但没渲染把content: 和display: block一起补上如果规则整个消失十有八九是选择器拼写。这个流程虽然简单但每次都能快速定位问题比对着代码猜半天高效得多。