滚动捕捉实现轮播图。
用 CSS 的 scroll-snap-type 与 scroll-snap-align 配合即可实现带原生惯性、触摸拖拽和键盘滚动的轮播图,JS 只需负责箭头跳页和滚动结束后同步圆点;一屏多张时需在子项 flex-basis 中计入 gap 并用 scroll-padding 处理留白,必要时用 scroll-snap-stop 与 proximity 避免跳页或抖动。
做轮播图不需要用 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






