前端知库

CSS Scroll Snap 怎样实现吸附滚动?

在滚动容器上设置 scroll-snap-type 指定方向和严格程度,在子元素上用 scroll-snap-align 指定对齐位置,滚动停下时浏览器就会停在对齐点上。scroll-padding 给固定头部留空间,scroll-snap-stop: always 防止快速滑动跳过某一项。

参考答案

简答

吸附滚动就是用户滑动一个列表或轮播图,松手后内容不会停在两张卡片中间,而是自动对齐到某一张卡片。以前要监听滚动事件再用 JS 计算位置,现在用 CSS Scroll Snap 几行样式就能做到。

它需要两边配合。滚动容器上写 scroll-snap-type,比如 x mandatory,规定沿哪个方向吸附、吸附得多严格;子元素上写 scroll-snap-align,比如 start 或 center,规定哪个位置对齐到容器。

常用的还有三个属性。容器上的 scroll-padding 给吸顶的头部留出空间,子元素上的 scroll-margin 单独调整某一项的间距,scroll-snap-stop: always 让快速滑动时不能跳过这一项。

要注意,mandatory 会强制停在吸附点上。如果某个子元素比容器还高,它超出的部分就可能永远滚不到,这种情况要用 proximity。

一个横向卡片列表

下面做一个横向滑动的卡片列表,松手后总有一张卡片对齐到左边缘:

CSS
.cards {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 16px;
}

.cards > .card {
  flex: 0 0 80%;
  scroll-snap-align: start;
}

容器首先得能滚动,所以要有 overflow-x: auto,内容也要比容器宽。没有溢出,就没有滚动,吸附也无从谈起。

scroll-snap-type: x mandatory 里的 x 是方向,也可以写 y、block、inline 或 both。mandatory 表示滚动停下时一定要落在某个吸附点上;换成 proximity,只有停在离吸附点足够近的地方才会吸过去,多近算近由浏览器决定。

子元素的 scroll-snap-align: start 表示卡片的起始边和容器的起始边对齐。轮播图常用 center,让当前卡片居中。这个属性默认是 none,没写的子元素不会成为吸附点。

scroll-padding-inline: 16px 把容器的吸附区域往里缩了 16px,所以卡片不会紧贴边缘,左边留出一点空隙。

页面有吸顶头部时怎样不被挡住

纵向整屏翻页时经常有一个 position: sticky 的头部。如果直接吸附到 start,每一节的标题都会被头部盖住。这时在容器上加 scroll-padding:

CSS
.scroller {
  height: 300px;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scroll-padding-top: 50px;
}

.scroller section {
  min-height: 100%;
  scroll-snap-align: start;
}

scroll-padding-top: 50px 让吸附位置整体往下让出 50px,头部占住的那一块就不算在对齐区域里。如果只有个别小节需要额外间距,就在那个子元素上写 scroll-margin,它和容器的 scroll-padding 可以叠加。

怎样防止快速滑动跳过内容

用户用力一划,可能一下滑过好几张卡片。如果每一张都必须看到,比如新手引导,可以在子元素上写:

CSS
.step {
  scroll-snap-stop: always;
}

默认值是 normal,允许跳过。always 让滚动在这个吸附点必须停一下。代价是用户没办法快速跳到想看的内容,所以普通列表不要随手加。

mandatory 的坑

MDN 特别提醒:子元素的内容会溢出容器时,不要用 mandatory。因为浏览器总要把滚动位置拉回吸附点,子元素超出的那部分用户可能滚不过去。

另外,在 mandatory 的容器里,如果某个子元素没有设置 scroll-snap-align,用户也没办法把它滚动到视图里。所以要么每个子元素都设置对齐方式,要么改用 proximity。

常见追问

怎样知道当前吸附到了哪一张,用来更新指示点?

Chrome 129 起提供了 scrollsnapchange 和 scrollsnapchanging 事件,事件对象上的 snapTargetInline、snapTargetBlock 就是吸附到的元素。Firefox 和 Safari 目前还不支持,所以只能当作渐进增强,不支持时可以用 IntersectionObserver 判断哪张卡片在可见区域。

吸附滚动能和滚动动画一起用吗?

能。吸附只决定滚动最后停在哪里,滚动过程中的视觉效果可以用滚动驱动动画来做,见《CSS 滚动驱动动画适合什么场景?》。