滚动捕捉实现轮播图。
做轮播图不需要用 JS 一帧帧算 `translateX`——只要给滚动容器加上 `scroll-snap-type`、给每张卡片加上 `scroll-snap-align`,浏览器就会自动把滑动停在整页位置,还自带原生惯性、触摸拖拽和键盘滚动。JS 在这套方案里只剩两个活:点箭头时滚到指定页,以及滚动结束后同步圆点高亮。
三行核心 CSS 就能跑起来
结论:滚动的容器负责"吸附"(snap),容器里的子项负责"被吸附的位置"(align),两者配对才生效,缺一个都不会有吸附效果。
`scroll-snap-type` 是加在滚动容器上的属性,写法是 `scroll-snap-type: x mandatory`,`x` 表示横向吸附,`mandatory` 表示"每次滚动必须停在某个吸附点"。`scroll-snap-align` 是加在每一张卡片上的属性,常见取值 `start`(卡片左边缘对齐容器左边缘)和 `center`(卡片居中)。
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch; /* 老 iOS 惯性滚动 */
scrollbar-width: none; /* Firefox 隐藏滚动条 */
}
.carousel::-webkit-scrollbar { display: none; }
.carousel .slide {
flex: 0 0 100%;
scroll-snap-align: start;
scroll-snap-stop: always; /* 快速滑动时不允许一滑跳过好几页 */
}
`scroll-snap-stop: always` 值得单独说一句:默认值 `normal` 下用户用力一甩可能连跳三张,加上它以后每张卡片都"挡"一下,翻页节奏更像传统轮播。
一屏多张、间距怎么处理
结论:一屏显示 N 张时,卡片的 `flex-basis` 必须把间距(gap)算进去,否则吸附点会累积偏移,滚到后面越来越不准。
假设一屏 2 张、间距 12px:
.carousel { gap: 12px; scroll-padding-left: 12px; }
.carousel .slide { flex: 0 0 calc((100% - 12px) / 2); }
.carousel { --per: 3; }
.carousel .slide { flex: 0 0 calc((100% - (var(--per) - 1) * 12px) / var(--per)); }
容器有左右 padding 时,用 `scroll-padding-left` / `scroll-padding-right` 告诉浏览器"吸附点相对留白后再对齐",不然第一张会被 padding 顶偏。
`mandatory` 和 `proximity` 的选择也有讲究:内容总宽度比容器短时,`mandatory` 会强行把内容吸附、导致滚不动或抖动,这时换成 `proximity`(靠近吸附点才吸),体验更稳。
JS 只剩两件事:跳页和同步圆点
结论:跳页用 `scrollTo` 配合卡片自身的 `offsetLeft` 计算最可靠,圆点同步交给 `IntersectionObserver` 或 `scrollend` 事件。
跳页不要自己乘宽度,直接读卡片位置:
const box = document.querySelector('.carousel');
const slides = [...box.children];
function goTo(i) {
box.scrollTo({ left: slides[i].offsetLeft - box.offsetLeft, behavior: 'smooth' });
}
圆点同步有两种做法。老牌做法是 `IntersectionObserver` 观察每张卡片,阈值取 `0.6` 左右,谁的交叉比例高谁就是当前页:
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.intersectionRatio > 0.6) setActive(slides.indexOf(e.target));
});
}, { root: box, threshold: [0.6] });
slides.forEach(s => io.observe(s));
新做法是监听 `scrollend` 事件(Chrome 114+ 支持),停止滚动时才计算一次 `Math.round(box.scrollLeft / 每页步长)`,性能更好。要兼容旧浏览器,就退回到 `scroll` 事件 + 150ms 防抖。
五个高频坑
结论:scroll-snap 的坑几乎都出在"尺寸不确定"和"吸附太强势"上。
- 图片未加载导致宽度跳动:给卡片固定 `aspect-ratio` 或写死高度,否则图片撑开后吸附位置全错。
- `mandatory` 卡死滚动:内容不足一屏时尤其明显,改 `proximity`。
- `scrollIntoView` 带偏整页:没有横向滚动祖先时它可能滚动外层页面,优先用容器的 `scrollTo`。
- 无障碍缺失:容器加 `aria-roledescription="carousel"`,箭头按钮写清 `aria-label="上一张"`,并支持左右方向键。
- 忽略减弱动效偏好:`@media (prefers-reduced-motion: reduce) { .carousel { scroll-behavior: auto; } }`,别让晕动症用户被迫看平滑滚动。
什么时候该放弃这套方案
结论:需要无限循环、自动播放进度条、或"3D 翻转"这类复杂过渡时,scroll-snap 就不是最优解。
scroll-snap 的本质是"原生滚动 + 对齐约束",它天然擅长图片/卡片浏览这类场景,移动端手感比 JS transform 方案好得多。但它没有真正的无限循环(首尾要复制克隆节点),自动播放要靠 `setInterval` 调 `scrollTo` 且用户手动滑动时得暂停——需求一旦超过这个复杂度,老老实实上 Swiper 更省事。
总的来说:一屏一张或几张、以浏览翻页为主的轮播,优先用 `scroll-snap-type` + `scroll-snap-align` 两行 CSS 打底,注意间距写进 `flex-basis`、谨慎使用 `mandatory`、跳页用 `offsetLeft`、同步用 `IntersectionObserver` 或 `scrollend`,就能用几十行代码做出比多数轮播库更顺滑的效果。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





