实战CSS Scroll Snapping

发布于 大漠

特别声明,本文根据@MAX KOHLER的《Practical CSS Scroll Snapping》一文所整理。

CSS Scroll Snap(CSS 滚动捕捉)允许你在用户完成滚动后多锁定特定的元素或位置。它非常适合构建像下面这样的交互:

浏览器支持和基本用法

自2016年推出CSS Scroll Snap以来,浏览器对它的支持有了显著的改善。Google 69+、Firefox、Edge和Safari都支持它的某些版本。

通过在容器元素上设置scroll-snap-type属性并在其子元素上设置scroll-snap-align属性来使用滚动捕捉。当滚动容器元素时,它将捕捉到你定义的子元素。最基本的结构像下面这样:

<!-- HTML -->
<div class='container'>
    <section class='child'></section>
    <section class='child'></section>
    <section class='child'></section>
    ...
</div>

// CSS

.container {
    scroll-snap-type: y mandatory;
}

.child {
    scroll-snap-align: start;
}

这与第一个版本的规范有所不同,后者允许你使用repeat关键词手动设置捕捉点:

.container {
    scroll-snap-points-y: repeat(300px);
}

这种方法非常有限。由于它只允许均匀间隔的捕捉点,因此你无法真正构建捕捉到不同大小元素的接口。如果元素在不同的屏幕尺寸之间改变形状,你也必然会遇到问题。

在撰写本文时,Firefox、IE和Edge支持上版本规范,而Chrome 69+ 和Safari支持新的规范,基于元素的语法。

你可以同时使用这两种方法(如果布局允许的话)来支持这两类浏览器:

.container {
    scroll-snap-type: mandatory;
    scroll-snap-points-y: repeat(300px);
    scroll-snap-type: y mandatory;
}

.child {
    scroll-snap-align: start;
}

我认为较好的做法是选择基于元素的语法,并加载一个Polyfill来支持还是不支持其它的浏览器。比如像下面的示例中使用的方法。

不幸的是,这个Polyfill没有提供浏览器包,如果你的项目中没有使用构建工程,那么使用这个Polyfill就会有点棘手。我找到一个最简单的方法是链接到bundle.run上的脚本,并在加载DOM之后,使用cssScrollSnapPolyfill()初始化它。值得指出的是,这个Polyfill仅支持基于元素的语法,而不支持repeat语法。

父容器属性

与任何属性一样,熟悉它们所接受的值是一个好主意。滚动捕捉属性应用于父元素和子元素,每个元素都有特定的值。就像Flexbox和Grid那样,父类变成了Flex或Grid容器。在这种情况下,父元素变成了快照(Snap)容器。

下面是父容器属性和值以及它们的工作方式。

scroll-snap-type: mandatory vs. proximity

mandatory属性值意味着每当用户停止滚动时,浏览器必须捕捉到捕捉点。proximity属性没有那么严格,它意味着浏览器可能会在看起来合适的情况下突然捕捉到某个位置。根据我的经验,当你在一个捕捉点的几百个像素内停止滚动时,这种情况就会出现。

在我们自己的工作中,我发现mandatory会提供更一致的用户体验,最它也可能是危险的,正如规范所指出的那样。想象一下这样的场景,滚动容器中的元素比视窗高:

如果将该容器设置为scroll-snap-type: mandatory,那么它将始终捕捉到元素的顶部或下面的元素的顶部,使得高元素的中间部分无法滚动。

scroll-padding

默认情况下,内容会吸附到容器的边缘。你可以通过在容器上设置scroll-padding属性来改变它。它遵循与常规padding属性相同的语法。

如果你的布局中有可能妨碍内容的元素(比如固定的标题),那么这个属性就非常的有用。

子元素上的属性

现在我们来看看运用在子元素上的属性。

scroll-snap-align

这个属性可以让你指定元素哪一部分应该与容器对齐。它有三个值可选择:startcenterend

这些是相对于滚动方向的。如果是垂直滚动,start指的是元素的顶部边缘。如果是水平滚动条,它指的是左边缘。centerend遵循相同的原则。你可以为滚动条的不同方向设置不同的值,这两个值之间用空格分隔开。

scroll-snap-stop:normal vs. always

默认情况下,滚动捕捉仅在用户停止滚动时启动,这意味着用户可以在停止之前跳过多个捕捉点。

你可以在任何子元素上设置scroll-snap-stop: always来改变它。这会强制滚动容器在该元素上停止,然后用户可以继续滚动。

在写这篇文章的时候,没有一个浏览器支持scroll-snap-stop

接下来让我们来看一些有关于CSS Scroll Snap的示例。

案例1:垂直列表

要使用垂直列表与每个列表元素对齐,只需要几行CSS。首先,我们告诉容器沿其垂直轴捕捉:

.container {
    scroll-snap-type: y mandatory;
}

然后,我们定义捕捉点。这里,我们指定每个列表元素的顶部将成为一个捕捉点:

.child {
    scroll-snap-align: start;
}

案例2:水平滑块

为了制作一个水平滑块,我们告诉容器沿着它的x轴对齐。我们还使用scroll-padding来确保子元素对齐到容器的中收。

.container {
    scroll-snap-type: x mandatory;
    scroll-padding: 50%;
}

然后,我们告诉容器哪个点被捕捉。为了使图库居中,我们将每个元素的中心点定义为一个捕捉点。

.child {
    scroll-snap-align: center;
}

案例3:垂直全屏

我们可以直接在<body>元素y轴上设置捕捉点:

body {
    scroll-snap-type: y mandatory;
}

然后,我们将每个部分的大小设置和视窗一样大,并将顶部边缘定义为捕捉点:

section {
    height: 100vh;
    width: 100vw;
    scroll-snap-align: start;
}

案例4:水平全屏

这个案例和前面的垂直全屏类似,但在x轴上使用了捕捉点。

body {
    scroll-snap-type: x mandatory;
}

section {
    height: 100vh;
    width: 100vw;
    scroll-snap-align: start;
}

案例5:2D图像网格

滚动捕捉可以同时在两个方向工作。同样,我们可以直接在<body>元素上设置scroll-snap-type

.container {
    scroll-snap-type: both mandatory;
}

然后,将每个.tile左上角定义为一个捕捉点:

.tile {
    scroll-snap-align: start;
}

关于用户体验的一些想法

摆弄滚动条是有风险的。由于它是与Web交互的一个基本部分,以任何方式更改它都会让一感觉不协调 —— 滚动劫持(Scrolljacking)这个术语就用来描述这种体验。

基于CSS Scroll Snap的好处在于,你不能直接控制滚动的位置。相反,你只是向浏览器提供一个位置列表,以适合平台、输入法和用户首选项的方式进行捕捉。这意味着你构建的滚动界面将与本机界面一样。

对我来说,这是CSS Scroll Snap在提供类似JavaScript库的一些类似功能特性。

以我的经验来看,这很有效,尤其在移动设备上。也许这是因为滚动捕捉已经是移动平台上的本机UI的一部分。(想象一下iOS和Android上的主屏幕 —— 它们本质上是带捕捉点的滑动块。)Android的Chrome上的交互特别好,因为它感觉像是常规滚动,但是视口总是在一个瞬间停下来:

肯定有一些奇特的数学方法实现这一切。多亏了 CSS Scroll Snap,我们不需要去探究那些复杂的数学方法。

当然,我们不应该开始对所有的事情都投扣分。像文章里面这样的东西没有它们也可以。但我认为在适当的情况下,它们可以是一个很好的增强 —— 图片库,幻灯片似乎是很好的选择,但可能还有更多的潜力。

CSS还提供了一些其他的属性可以用来帮助我们改善用户体验,如果感兴趣,可以阅读《滚动的特性》和《改变用户体验的滚动新特性》。

总结

如果考虑周全,滚动捕捉可以是一个有用的设计工具。CSS Scroll Snap Points允许你连接到浏览器的本机滚动交互,因此你的界面感觉无缝且平滑。随着JavaScript API的出现,这些功能将变得更加强大。不过,触摸(touch)或许是正确的做法。

扩展阅读