伪元素动画并不是简单地把 animation 写在 ::before 或 ::after 上就能稳定生效。它们作为虚拟盒子,能应用 transform、opacity、background-position 等属性动画,但前提是必须正确设置 content、display 与定位上下文。很多效果失败不是 keyframes 写错,而是伪元素根本没有生成,或者层叠顺序与 transform 覆盖问题没有被注意到。下面拆解具体写法,并给出按钮扫光、下划线滑动、双点加载三类可直接使用的示例。

一、伪元素参与动画的前提条件
伪元素最大的特殊性在于它不会凭空出现,必须通过 content 属性声明一个盒模型,哪怕内容为空也要写成 content:""。如果漏掉这一步,浏览器不会渲染 ::before 或 ::after,后续的 animation 自然完全无效。对于宽度、高度和定位动画,伪元素默认的 display: inline 也会限制盒模型表现,因此通常需要显式设置 display: block 或 display: inline-block,再配合 position: absolute 让它在宿主元素内自由移动。
宿主元素需要建立定位上下文,常用的做法是给宿主设置 position: relative,然后伪元素设置 position: absolute。这样 keyframes 里的 left、top 或 transform 才有明确参照。比如下面基础示例在伪元素中生成一个彩色小方块,并让它横向往返滑动:
.box { position: relative; width: 220px; height: 80px; background: #f1f5f9; }
.box::before { content: ""; position: absolute; left: 0; top: 0; width: 30px; height: 30px; background: #4f46e5; animation: slide 2s ease-in-out infinite alternate; }
@keyframes slide { from { transform: translateX(0); } to { transform: translateX(190px); } }
这个示例中如果删除 content: "" 或者忘记设置 position: absolute,动画都不会按预期呈现。另一个关键点是 animation 可以简写,也可以拆分为 animation-name、animation-duration、animation-timing-function 等独立属性。当需要同时控制延迟、次数和方向时,拆分写法更容易维护,尤其适合多个伪元素组合的场景。
二、按钮扫光与下划线滑动
扫光效果是 after 伪元素动画的典型应用。宿主按钮设置 overflow: hidden 裁剪超出边界的高光条,after 生成一条倾斜的半透明白色块,通过 keyframes 控制它的横向位移。相比在按钮内部增加 span,伪元素方案不需要改动任何 HTML 结构,也不会影响按钮内部文字的可访问性。
.btn { position: relative; overflow: hidden; padding: 12px 28px; background: #2563eb; color: #fff; border: none; border-radius: 6px; }
.btn::after { content: ""; position: absolute; top: 0; left: -60px; width: 40px; height: 100%; background: rgba(255, 255, 255, 0.35); transform: skewX(-20deg); animation: shine 2.4s ease-in-out infinite; }
@keyframes shine { 0% { left: -60px; } 60%, 100% { left: 120%; } }
这里的位移使用 left 属性实现,简单直观,但 left 动画会触发布局重排,对性能有要求的页面建议改成 transform。需要注意的是,after 的初始样式中已经有 skewX(-20deg),如果 keyframes 只写 transform: translateX(300px),会丢掉倾斜效果。正确的写法是把 skew 一起带入,例如 transform: translateX(300px) skewX(-20deg)。这是伪元素动画中最容易被忽略的覆盖问题。
下划线滑动同样是常见交互。它更多使用 transition 来响应 hover,但也可以用 animation 配合 forwards 实现进入动画。基础做法是由 after 生成一条与文字等宽的下划线,初始 scaleX(0),触发悬停时动画到 scaleX(1),并通过 transform-origin 控制从左侧还是右侧出现。animation 的优势在于可以定义多阶段关键帧,比如先缩小再回弹,比 transition 的单次过渡更有表现力。实际项目中可以根据是否需要循环播放来决定采用哪一种方案。
三、组合多个伪元素制作加载指示器
一个宿主元素同时拥有 before 和 after,两个伪元素可以分别指定动画,这是实现轻量加载指示器的常用思路。例如双点加载效果中,宿主本身只是一个占位容器,before 和 after 各自生成一个圆点,通过不同的 animation-delay 让它们交替上下跳动。由于两个动画共用同一组 keyframes,代码量很少,且不需要引入额外节点。
.loader { position: relative; width: 60px; height: 24px; }
.loader::before, .loader::after { content: ""; position: absolute; top: 0; width: 16px; height: 16px; border-radius: 50%; background: #f97316; animation: bounce 1.2s ease-in-out infinite; }
.loader::before { left: 0; }
.loader::after { left: 40px; animation-delay: 0.6s; }
@keyframes bounce { 0%, 100% { transform: translateY(0); opacity: 0.8; } 50% { transform: translateY(-14px); opacity: 1; } }
设置 animation-delay 为周期的一半,可以让两个点形成交替节奏。如果需要两个伪元素执行完全不同的动画,则不能把动画简写在同一个分组选择器里,因为后写的 animation 会覆盖前面的,而应该分别给 before 和 after 设置各自的 animation-name 和对应属性。伪元素之间还存在自然层叠顺序:before 是宿主第一个子元素,after 是最后一个子元素,所以未设置 z-index 时 after 通常显示在 before 上方。遇到遮挡问题时应优先检查这里,而不是盲目修改动画参数。
这种组合方式还可以扩展出更多状态,比如鼠标悬停时暂停动画,通过 .loader:hover::before, .loader:hover::after { animation-play-state: paused; } 即可实现。也可以在同一个伪元素上串联多个动画,例如同时执行位移和缩放,只需在 animation 属性中用逗号分隔多个动画名称,同时 keyframes 各自独立定义,这样可以做出更自然的弹性效果。
四、伪元素动画的常见误区与性能优化
一个常见的认识误区是试图让 content 属性参与补间动画。例如希望伪元素文字在 ABC 和 XYZ 之间渐变切换,但实际上 content 是离散属性,浏览器不会对内容字符串做插值,动画只会在关键帧瞬间跳变。如果确实需要文字切换,可以考虑使用 steps() 时间函数做出逐帧效果,但这种情况已经不属于平滑动画,适合做轮播文字或指示符变化,不应作为补间方案。
另一个高频问题是 transform 覆盖。伪元素的基础样式可能已经设置了 rotate、skew 或 scale,当 keyframes 里写 transform 时,旧值不会自动合并。比如初始 transform: rotate(45deg),动画 from { transform: translateX(0); } 到 to { transform: translateX(80px); },实际执行时旋转会丢失。解决方式是在关键帧中把完整变换都写出来,或者用独立的 CSS 变量保存共享变换部分,减少重复定义。
性能层面,优先选择 transform 和 opacity 做动画,因为它们通常由合成器处理,不会频繁触发布局和重绘。left、width、height、margin 等属性动画则可能引起重排,在移动端低配置设备上容易掉帧。对于需要频繁运动的伪元素,可以配合 will-change: transform 提前建立合成层,但不要对大量元素同时使用,否则会占用过多内存。此外,伪元素不应该承载关键交互信息,屏幕阅读器对伪元素的识别支持有限,重要内容仍应放在真实 DOM 元素中。