CSS Scroll Snap:让滚动整齐地停靠
横向 carousel、整屏分页、图片画廊——希望滚动结束时停在整齐的位置,而不是卡在两张图之间。CSS Scroll Snap 让滚动在结束时自动吸附到预设的对齐点,纯 CSS,不必用 JS 监听 scroll 反复计算与 scrollTo。
1 · 三个角色,五个属性
整套机制围绕三个角色展开——容器、容器里的对齐基准区、子项各自的吸附区:
| 属性 | 设在 | 作用 |
|---|---|---|
scroll-snap-type
|
snap-container(滚动容器) | 开启吸附,并选轴(x / y / block / inline / both)与强度(mandatory / proximity)。 |
scroll-padding
|
snap-container | 把容器可视区内缩成 snapport(对齐基准区),给固定栏 / 留白让位。 |
scroll-snap-align
|
snap-area(子项) | 子项用 start / center / end 哪个点去对齐 snapport;none 让该子项不参与吸附。 |
scroll-margin
|
snap-area(子项) | 把子项边框盒外扩成它的 snap-area,微调吸附判定与落点。 |
scroll-snap-stop
|
snap-area(子项) | normal = 一次手势可掠过多个吸附点;always = 遇到吸附点就必须停,不许划过。 |
术语:snapport = 容器可视区被 scroll-padding 内缩后的框,吸附时子项的对齐点要落到它的边 / 中心;snap-area = 子项边框盒被 scroll-margin 外扩后的框,scroll-snap-align 决定用它的哪个点。
2 · 动手:横向 carousel
横向拖动 / 滚动下框,松手看它吸附到哪。改各项参数再试——尤其留意 mandatory 与 proximity 的手感差别,以及 scroll-padding(容器内缩,虚线)与 scroll-margin(子项外扩)如何挪动吸附落点。
3 · scroll-snap-align 的完整写法
它不止 start 一个值,实际可写一到两个、还能退出:
.item { scroll-snap-align: center; }
.item-2 { scroll-snap-align: start center; }
.item-x { scroll-snap-align: none; }
- 单值(如
center):block 与 inline 两轴都用这个对齐。 - 双值(如
start center):block 轴在前、inline 轴在后,两轴分别对齐。 none:该子项不作为吸附点——同一容器里混着设,可让大多数子项吸附、个别子项自由停。
4 · mandatory 还是 proximity
mandatory:滚动结束总是停在最近的吸附点——整齐,但内容比视口高 / 宽时,夹在两个吸附点之间的部分可能无法停留查看。适合「每屏一页」「每次一张」的场景。proximity:只有滚动停在吸附点附近时才吸附,否则保留用户停的位置——更宽容,适合长内容画廊。不确定就用它。
5 · 纵向整屏分页
把轴换成 y、让每个 section 占满容器高度(height: 100%)、scroll-snap-align: start,就得到「一屏一页」的纵向分页(短视频流、落地页分屏都是这个套路)。下框纵向滚动试试:
轴关键字:x / y 是物理轴,block / inline 是逻辑轴(随 writing-mode 走),both 让两轴同时吸附(网格画廊)。
6 · 用事件知道「吸到了谁」
过去要知道当前停在第几张,只能监听 scroll 自己算 scrollLeft。现在有两个原生事件直接在滚动容器上触发,事件对象(SnapEvent)带 snapTargetBlock / snapTargetInline,直接给出吸附到的子元素(上面的横向 carousel demo
已用它读出「吸附到」与「滚动中预选」,并在 readout 里标出你的浏览器是否支持):
scrollsnapchanging:滚动过程中,预选的吸附目标变化时触发(上面 demo 的「滚动中预选」)。scrollsnapchange:滚动结束、吸附目标最终确定时触发(上面 demo 的「吸附到」),紧挨在scrollend之前。
const box = document.querySelector('.carousel');
box.addEventListener('scrollsnapchange', (e) => {
e.snapTargetInline.classList.add('active');
});
box.addEventListener('scrollsnapchanging', (e) => {
preview(e.snapTargetInline);
});
7 · 纯 CSS carousel:圆点指示器与翻页按钮
更进一步,圆点指示器和左右翻页按钮过去都得写 JS,如今也能纯 CSS 生成:容器设 scroll-marker-group: after 自动在其后生成一组标记,每个子项的 ::scroll-marker 是一个标记(点),当前项用 :target-current 高亮;::scroll-button()
生成可点的上一张 / 下一张按钮。下面这排圆点是纯 CSS、点它能跳页,没有一行 JS:
.carousel {
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-marker-group: after;
}
.carousel > li { scroll-snap-align: center; }
.carousel > li::scroll-marker {
content: "";
width: 12px; height: 12px; border-radius: 50%;
background: #fff; border: 2px solid #bac8ff;
}
.carousel > li::scroll-marker:target-current {
background: #4263eb;
}
.carousel::scroll-button(left) { content: "‹" / "上一张"; }
.carousel::scroll-button(right) { content: "›" / "下一张"; }
.carousel::scroll-button(*):disabled { opacity: .35; }
8 · 边界与细节
- 吸附发生在滚动结束那一刻,不打断滚动过程;
scroll-snap-type设在容器、对齐属性设在子项,缺一不可。 mandatory配上「比视口大的子项」要谨慎——中间内容可能永远滚不到、停不住。scrollsnapchange/scrollsnapchanging(Chrome 129+)与::scroll-marker/::scroll-button()(Chrome 135+)较新,需做渐进增强:不支持时退回手算 scroll、退回普通滚动容器(无圆点 / 按钮),核心吸附照常工作。