⭐ 推荐:社区规则条款 V1.0

滚动捕捉实现轮播图。

不能说的秘密
不能说的秘密 星耀SVIP星耀SVIP
发布于 2026-08-03 15:29 ·71 浏览 ·0 回复
内容摘要

用 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 的坑几乎都出在"尺寸不确定"和"吸附太强势"上。

  1. 图片未加载导致宽度跳动:给卡片固定 aspect-ratio 或写死高度,否则图片撑开后吸附位置全错。
  2. mandatory 卡死滚动:内容不足一屏时尤其明显,改 proximity。
  3. scrollIntoView 带偏整页:没有横向滚动祖先时它可能滚动外层页面,优先用容器的 scrollTo。
  4. 无障碍缺失:容器加 aria-roledescription="carousel",箭头按钮写清 aria-label="上一张",并支持左右方向键。
  5. 忽略减弱动效偏好:@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,就能用几十行代码做出比多数轮播库更顺滑的效果。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-411.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~