本质是位移后瞬间跳回起点,需双份内容拼接、精确计算位移距离、用linear缓动、overflow:hidden裁剪,并配合will-change与backface-visibility启用gpu加速。

用 @keyframes + animation-iteration-count: infinite 实现基础循环
无限滚动的本质是让元素位移后瞬间跳回起点,视觉上不中断。CSS 动画最直接的方式就是定义一个从 0% 到 100% 的位移关键帧,并设为无限重复。
常见错误是只写 transform: translateX(-100%),结果内容滚到一半就卡住或闪回——这是因为没控制好“无缝衔接点”。必须确保动画结束时,元素刚好完全移出视口,且下一帧起始位置与初始状态完全重合。
- 动画持续时间(
animation-duration)要和内容总宽度、滚动速度匹配;太快会看不清,太慢易察觉停顿 - 必须用
linear动画函数,避免ease类缓动导致速度不均、衔接露白 - 容器需设
overflow: hidden,否则滚动过程会看到溢出内容
@keyframes scroll-loop {
0% { transform: translateX(0); }
100% { transform: translateX(-500px); } /* 必须等于单条内容宽度 × 条数 */
}
用双份内容拼接解决“跳变”问题
单份内容滚动到末尾时,translateX(-100%) 后再回到 0%,浏览器渲染有微小间隙,人眼容易感知“一顿”。真正无缝的做法是准备两份相同内容,首尾相接,让动画位移距离刚好等于一份内容宽度,滚动过程中始终有内容填满视口。
典型场景:滚动新闻栏、产品 slogan 横幅、股票行情跑马灯。此时 animation-duration 决定的是“滚动完一份内容所需时间”,不是整段动画周期。
- HTML 中把所有滚动项复制一遍:
<div class="marquee"> <span>A</span><span>B</span><span>C</span><span>A</span><span>B</span><span>C</span> </div> - CSS 中
@keyframes的终点值设为-100%或具体像素(如-300px),对应第一份内容总宽 - 不能用
%值做位移除非父容器宽度固定,否则响应式下会错位
用 will-change: transform 和 backface-visibility: hidden 防掉帧
长列表滚动+无限动画容易在低端设备或 Safari 上出现卡顿、闪烁甚至文字模糊。这不是代码逻辑错,而是浏览器没启用 GPU 加速或触发了层合成异常。
这两个声明不是可有可无的优化项,而是 iOS Safari 和旧版 Chrome 下能否稳定跑起来的关键补丁。
-
will-change: transform提前告诉浏览器该元素将频繁变换位置,分配独立图层 -
backface-visibility: hidden强制开启硬件加速,同时修复部分 Android WebView 文字锯齿 - 不要对整个
.marquee容器加,只加在实际做transform的子元素上(比如内部的<span></span>或包裹滚动项的<div>)<h3>响应式下用 JS 动态重设 <code>animation-duration避免速度突变纯 CSS 方案在视口缩放时,内容宽度变了,但动画时间没变,导致滚动忽快忽慢。尤其移动端横竖屏切换时特别明显。
解决思路不是放弃 CSS 动画,而是用 JS 监听尺寸变化,只动态改一个 CSS 变量,再让动画时长通过
calc()关联它。- 在根元素定义变量:
:root { --scroll-speed: 20s; } - CSS 中写:
animation-duration: calc(var(--scroll-speed) * 1s); - JS 里用
window.matchMedia或ResizeObserver检测宽度变化,按比例更新--scroll-speed - 别用
setTimeout节流 resize,ResizeObserver更精准,且不会丢帧
真正难的不是写出能动的代码,而是让不同屏幕、不同设备、不同缩放比例下,滚动节奏都一致——这需要把 CSS 变量、JS 监听、动画函数三者咬合在一起,少一个环节就容易在某个机型上露馅。
- 在根元素定义变量:











