【CSS】---- 纯 CSS 实现无限滚动轮播:从 keyframes 到无缝衔接的完整拆解 1. 纯 CSS 无限滚动轮播到底解决什么问题适合哪些场景纯 CSS 无限滚动轮播指的是不写一行 JavaScript只靠keyframes位移配合复制一份轨道内容让图片、公告、品牌 Logo 在容器里持续向左滚动并且首尾衔接处看不出断点。它能做的事情很具体活动页顶部的滚动横幅、后台系统的公告栏、官网底部的合作品牌墙、大屏看板里的循环指标卡都可以直接套用。适合的人群也很明确前端开发者、写活动页的运营同学、做 Taro 或 uni-app 的移动端同学以及任何不想为了一个「滚动」引入 swiper 这类库的人。我试过在一个纯静态活动页里用这套方案整个页面零 JS 依赖加载完就开始滚连DOMContentLoaded都不用等。核心检索词就是「纯 CSS 无限滚动轮播」它的本质是三个东西的组合一个overflow: hidden的视口容器、一条被复制成两份的轨道、一段从translateX(0)到translateX(-50%)的线性无限动画。理解了这个组合后面所有参数调整都是在这三件套上做加减法。为什么值得用纯 CSS 而不是 JS第一性能上动画跑在合成层transform不触发重排重绘比setInterval改left顺滑得多第二代码量极小一个keyframes加几行 flex 就够第三没有定时器泄漏、没有组件卸载清理的负担SSR 场景下也不会因为 JS 未水合而闪一下。当然它也有边界不能手动拖拽、不能点击切换、不能动态增删卡片后自动重算这些场景还是得回到 JS。所以选型判断很简单——只要内容是「自动循环展示、用户不需要交互」纯 CSS 就是最优解。下面我会从最基础的 DOM 结构讲起一路拆到三种不同的位移写法把速度、间距、悬停暂停、首尾对齐这些坑全部铺开。每一段配置都可以直接复制改个类名就能用。文中涉及模型调用或接口调试的部分我会顺带说明怎么用 TaoToken 的 API Key 在本地验证请求方便你把页面里的动态数据也接上。2. 动手前的准备容器、轨道与复制份数的结构设计在写动画之前必须先把 DOM 结构定死因为纯 CSS 方案对结构的依赖极强结构错了动画再对也白搭。整个结构分三层最外层是视口viewport负责裁剪中间是轨道track负责整体位移最内层是内容组group每一组里放一屏的卡片。关键点在于轨道里要放两组完全相同的内容第二组是第一组的复制品这样当第一组完全移出视口时第二组刚好补上形成无缝衔接。先看视口容器。它的职责只有一个把超出部分藏起来。所以overflow: hidden是必须的同时它要能横向容纳轨道通常用display: flex让轨道作为唯一子元素自然铺开。这里有个容易忽略的点视口本身不要设width为固定像素用100%或100vw都行但一旦设了固定宽度在响应式下就会露馅。.carousel-viewport { overflow: hidden; display: flex; width: 100%; }接着是轨道。轨道是真正执行动画的元素它内部包含两组内容。这里有两种流派一种是把两组内容直接作为轨道的两个子元素轨道用display: flex横排另一种是把两组内容包在一个更内层的 wrapper 里只动 wrapper。第一种更直观第二种性能更好只动一个元素。我先把第一种讲透因为它是理解原理的必经之路。.carousel-track { display: flex; width: 100%; will-change: transform; animation: scroll-left 20s linear infinite; }will-change: transform这行是给浏览器的提示告诉它这个元素会频繁做 transform 变换提前把它提升到合成层。实测下来在低端安卓机上加了这行滚动明显更稳不加偶尔会有一帧抖动。但它不是越多越好滥用会吃内存只加在真正做动画的轨道上就行。然后是内容组。每一组要保证至少占满一屏否则会出现「一组还没滚完第二组已经进来了」的错位。用flex: 0 0 100%强制每组占满视口宽度这是无缝的前提。组内用 grid 排卡片三列还是四列看设计稿间距用gap统一控制。.carousel-group { flex: 0 0 100%; display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; padding-right: 20px; box-sizing: border-box; } .carousel-item { width: 100%; aspect-ratio: 1 / 1; border-radius: 24px; object-fit: cover; }注意padding-right: 20px这一行。因为两组内容之间是靠 flex 紧挨着的如果不加右边距第一组最后一张卡片和第二组第一张卡片之间就会少一个gap的距离视觉上突然挤在一起。这个坑我在第一次写的时候踩得很实滚动到衔接处会明显「顿」一下。加上padding-right等于gap的值衔接处的间距就和组内间距一致了。box-sizing: border-box保证 padding 不会把组撑出 100% 宽度。DOM 结构对应如下两组内容完全一致图片地址、类名都复制一份div classcarousel-viewport div classcarousel-track div classcarousel-group img classcarousel-item srca.jpg alt / img classcarousel-item srcb.jpg alt / img classcarousel-item srcc.jpg alt / /div div classcarousel-group aria-hiddentrue img classcarousel-item srca.jpg alt / img classcarousel-item srcb.jpg alt / img classcarousel-item srcc.jpg alt / /div /div /div第二组加aria-hiddentrue是给读屏软件的因为它是纯视觉复制品不该被重复朗读。这个细节很多人漏掉但对无障碍是实打实的加分。到这里结构就定完了。你可以先在浏览器里打开此时两组内容会并排显示第二组在视口外。如果看到第二组露出来了说明flex: 0 0 100%没生效检查是不是被其他样式覆盖了宽度。结构对了动画才有意义。3. 三种 keyframes 位移写法与可复制配置动画部分是这个方案的核心也是差异最大的地方。同样是无限滚动至少有三种位移写法理解它们的区别你就能根据场景挑最合适的。我按从易到难、从差到优的顺序讲。第一种两组各自执行translateX(-100%)。轨道不设动画两个 group 各自跑自己的动画位移距离是自身宽度的 100%。动画结束时第一组刚好完全移出第二组刚好补位。这种写法最直观但需要给两个 group 分别写动画代码重复而且两组动画必须严格同步稍有偏差就错位。.carousel-group { animation: scroll-one 20s linear infinite; } keyframes scroll-one { to { transform: translateX(-100%); } }第二种轨道执行translateX(-200%)但第二组延迟半个周期。这是某些官网在用的写法位移是自身两倍动画时长拉长到 200s第二组用animation-delay: -100s提前一半时间启动。它的问题是理解成本高延迟时间必须精确等于周期的一半改速度时两个值都要动容易算错。.carousel-track { animation: scroll-two 200s linear infinite; } .carousel-group:last-child { animation: scroll-two 200s linear infinite; animation-delay: -100s; } keyframes scroll-two { to { transform: translateX(-200%); } }第三种也是我最终推荐的只动轨道位移translateX(-50%)。因为轨道里装了两组等宽内容轨道总宽是单组的两倍移动自身宽度的 50% 正好等于移动一组内容的宽度。动画从 0 到 -50%第一组移出、第二组补位循环时瞬间回到 0视觉上完全无缝。只需要一个动画、一个元素性能最好参数最好算。.carousel-track { --duration: 20s; display: flex; width: 100%; will-change: transform; animation: scroll-seamless var(--duration) linear infinite; } keyframes scroll-seamless { from { transform: translateX(0); } to { transform: translateX(-50%); } }用 CSS 变量--duration把时长抽出来改速度只改一个值。linear是关键不能用ease否则滚动会忽快忽慢衔接处会露馅。infinite保证无限循环。速度怎么定经验值是一屏三张卡片、每张 300px 左右时20s到30s比较舒服太快看不清内容太慢显得卡。品牌墙 Logo 多的话可以放到40s。间距用gap控制16px到24px是常见区间移动端可以缩到12px。悬停暂停是活动页的高频需求用户鼠标移上去要能停下来看。纯 CSS 用animation-play-state一行搞定.carousel-viewport:hover .carousel-track { animation-play-state: paused; }注意是挂在视口上而不是轨道上因为鼠标进入视口区域就该暂停挂在轨道上如果轨道有 padding 会有边缘不灵敏的问题。移动端没有 hover这条规则自然不生效滚动继续符合预期。如果你想让页面里的滚动内容来自接口比如公告栏文案是后端返回的那就需要先拿到数据再渲染。这时候可以用 TaoToken 的 API 在本地先验证接口通不通。它的 Base URL 是https://taotoken.net/api在控制台创建 Key 后用 curl 测一下curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 返回一段公告文案}] }拿到返回后把文案塞进carousel-item即可。Key 的创建入口在控制台的 API Keys 页面文档在接入文档里模型对话可以直接在网页上试。这套流程和纯 CSS 轮播是解耦的轮播只负责展示数据从哪来都行。4. 浏览器验证无缝衔接与首尾帧对齐怎么检查写完配置别急着上线无缝衔接这件事必须亲眼验证因为很多错位在静态截图里看不出来只有滚动到衔接点才暴露。我总结了几个必查项按顺序过一遍基本能排干净。第一检查轨道总宽是否等于两组内容之和。打开 DevTools选中.carousel-track看它的scrollWidth。如果两组内容各占一屏轨道宽度应该约等于视口宽度的两倍。如果明显小于两倍说明某一组没占满flex: 0 0 100%可能被覆盖了。这一步是根基根基不对后面全错。第二检查动画终点的位移量。在 Elements 面板里选中轨道切到 Computed 或直接看动画translateX(-50%)对应的像素值应该正好等于一组内容的宽度。如果不等说明轨道宽度和预期不符。可以用一个临时样式把动画暂停在终点验证.carousel-track { animation-play-state: paused; transform: translateX(-50%); }手动加上这两行看第二组的第一张卡片是否正好落在视口最左侧、和第一组第一张卡片起始位置重合。重合就说明首尾帧对齐了不重合就是宽度算错了。第三检查衔接处的间距。把动画暂停在-50%附近观察第一组最后一张卡片和第二组第一张卡片之间的距离应该和组内gap一致。如果偏小就是前面说的padding-right没加或值不对。这个坑非常隐蔽因为滚动速度快的时候肉眼几乎看不出一暂停就原形毕露。第四检查有没有「跳帧」。让动画正常跑盯着衔接点看几秒。如果每次循环回到起点时画面「闪」一下或「跳」一下通常是两个原因一是linear写成了别的缓动二是from和to的位移量不是精确的组宽。用translateX(-50%)的写法基本不会跳因为 50% 是相对轨道自身算的天然精确。第五跨浏览器验证。Chrome 和 Safari 对will-change和合成层的处理略有差异Safari 上偶尔会出现动画启动瞬间的闪烁。如果遇到可以给轨道加backface-visibility: hidden和transform: translateZ(0)强制开启硬件加速.carousel-track { backface-visibility: hidden; transform: translateZ(0); }第六移动端验证。在真机上滑一下重点看滚动是否跟手、有没有掉帧。如果掉帧先确认will-change加了再确认卡片图片有没有设固定尺寸导致布局抖动。图片一定要给aspect-ratio或固定宽高否则图片加载完成前后轨道宽度会变动画位移就错位了。验证通过后建议把--duration做成响应式移动端慢一点、桌面端快一点观感更好.carousel-track { --duration: 30s; } media (min-width: 768px) { .carousel-track { --duration: 20s; } }这套验证流程走完基本可以保证上线后不会出现「滚着滚着错位」的尴尬。下面把常见报错和排查方法集中列一下。5. 常见错误排查从错位到卡顿的对照表纯 CSS 轮播的报错不像接口那样有明确的错误码它更多是「看起来不对」所以排查要靠对照现象找原因。我把踩过的坑整理成一张对照表遇到问题直接查。现象可能原因修复方式滚动到衔接处突然挤在一起组间缺少padding-right给.carousel-group加padding-right等于gap第二组内容露出在视口外flex: 0 0 100%未生效检查宽度是否被覆盖加box-sizing: border-box动画循环时画面跳动缓动不是linear或位移量不精确改linear用translateX(-50%)滚动卡顿、掉帧未开启合成层加will-change: transform和translateZ(0)图片加载后错位图片无固定尺寸导致轨道宽度变化给图片设aspect-ratio或固定宽高悬停不暂停animation-play-state挂错元素挂到视口容器的:hover上Safari 启动瞬间闪烁合成层处理差异加backface-visibility: hidden移动端滚动方向异常误用了translateY或写反符号确认是translateX且为负值除了这些视觉问题如果你在页面里同时调用了模型接口来填充内容可能会遇到接口层的报错。比如401 Unauthorized通常是 Key 没带或带错检查请求头Authorization: Bearer key是否完整。local proxy failed一般出现在本地开发代理配置上检查代理地址是否指向了正确的 Base URL。reading choices这类报错多半是解析返回结构时字段名写错先打印完整响应再取字段。OAuth 相关的报错则要确认授权流程是否走完、token 是否过期。这里要强调一个配置三件套的概念无论你用哪种方式接入模型Base URL、API Key、Model ID 这三样必须同时正确缺一个都会失败。Base URL 用https://taotoken.net/apiKey 从控制台 API Keys 页面拿Model ID 按文档里列出的填。三者对齐后请求才能通。如果你用的是 Claude Code 这类编码工具配置通常写在 settings 文件里格式类似{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: your-key-here, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }Codex 的auth.json则是另一套结构但核心字段还是这三样。Cline 的 MCP 配置里同样要填 Base URL、Key、Model ID。不管哪个工具只要这三件套对了连接就稳。遇到连接问题先核对这三样再去看网络和权限。排查的顺序建议是先看结构宽度对不对再看动画位移对不对最后看环境接口通不通。结构问题占八成动画问题占一成半接口问题占半成。按这个顺序查效率最高。6. 把轮播接上真实数据与长期使用建议纯 CSS 轮播本身不依赖任何后端但真实项目里内容往往来自接口。最常见的做法是页面加载后请求数据渲染出两组相同的 DOM再让 CSS 动画接管。这里的关键是动画必须在两组 DOM 都渲染完成后再启动否则轨道宽度不对位移就错。如果数据是异步的可以给轨道加一个data-ready属性CSS 里用属性选择器控制动画.carousel-track[data-readytrue] { animation: scroll-seamless var(--duration) linear infinite; }数据回来、两组 DOM 都插入后用 JS 把data-ready设成true动画才开始。这样避免了「先滚起来再补内容」的错位。虽然引入了一点点 JS但只是控制开关滚动本身还是纯 CSS性能不受影响。如果你的项目需要频繁调整滚动内容比如运营每天换公告建议把卡片数据抽成一个数组渲染时map两遍第二遍加aria-hidden。这样改内容只改数据源结构不用动。速度、间距这些视觉参数全部走 CSS 变量运营改不了代码也能通过主题配置调。长期使用还有几个建议。第一卡片数量别太多一组 6 到 10 张足够太多会让轨道很长动画位移计算虽然还是 50%但首屏渲染压力大。第二图片一定要压缩并设固定尺寸aspect-ratio是好朋友。第三给视口加contain: layout paint可以进一步隔离渲染减少页面其他部分对轮播的影响。第四如果页面里有多个轮播给每个轨道的--duration设不同值避免所有轮播同步滚动显得呆板。对于需要长期跑编码任务、Agent 调用的场景可以考虑用 Coding Plan把模型调用和额度管理统一起来省得每次手动配 Key。日常验证模型是否可用直接在模型对话页面试一句就行。接入细节和参数说明都在接入文档里控制台的 API Keys 页面负责创建和管理密钥。这套组合用下来前端展示归前端模型调用归模型各司其职维护起来清爽。最后回到轮播本身三种位移写法里translateX(-50%)的单元素方案是综合最优解代码最少、性能最好、参数最好算。把结构、动画、验证三步走完你就能在活动页、公告栏、品牌墙里零 JS 依赖地跑起无限滚动。真正上手改一遍参数比看十篇文章都管用。