图解CSS:CSS滚动捕捉(Part1)

发布于 大漠

CSS 滚动捕捉是 W3C 中 CSS Scroll Snap Module Level 1 模块,可以用来创建一个可滚动的容器,可用来替代以往依赖 JavaScript 脚本来创建滑块组件。也就是说,使用该特性(CSS 的滚动捕捉特性)是快速将元素制作成可滚动容器的一种简单快速的方法。另外,CSS 滚动捕捉特性还可以帮助我们给用户更好的体验。接下来和大家一起来探讨这个 CSS 新特性,即 CSS 滚动捕捉。

简介

CSS 滚动捕捉(CSS Scroll Snap)规范为我们提供了一种方法,可以在用户滚动浏览文档时,将其滚动到特定的点(位置)。这对于在移动设备上甚至在PC端上为某些类型的应用程序(比如滑块组件)创造一个更类似于应用程序(原生客户端)的体验是很有帮助的。

简而言之,CSS 滚动捕捉可以:

  • 防止滚动时出现尴尬的滚动位置
  • 创建更好的滚动体验

CSS 滚动捕捉可能涉及到的知识如下图所示:

基础知识

在开始聊 CSS 滚动捕捉之前,先花一点时间来了解一些基础知识,这样有利于大家更易于理解我们今天要理解的 CSS 滚动捕捉。

容器

在 CSS 的世界中,任何一个 HTML 元素都是一个盒子(Box),盒子的概念是一个抽象的概念,由 CSS 引擎根据文档中的内容所创建,主要用于元素的定位、布局和格式化等用途。在 CSS 中时常用 盒模型 来描述盒子的具体参数:

盒子之间的类型可以使用 CSS 的 display 属性的不同值来进行切换。也就是说,使用 display 属性来格式化盒子,即创建不同的盒子,好比类似下图这样的器皿:

使用 display 属性格式化盒子之时就给不同的盒子使用了不同的 格式化上下文模型

在 CSS 中,每个盒子又被称之为 容器。其主要用来放置内容。正如《图解CSS: 元素尺寸的设置》所介绍的一样可以使用内部尺寸(比如 min-contentmax-content)和外部尺寸(比如 widthheight等)属性来设置容器的大小。更多的开发者还是比较喜欢使用外部尺寸来描述一个容器的大小:

上表中的外部尺寸属性涵盖了物理属性和逻辑属性,有关于 CSS 逻辑属性更多的介绍可以阅读《图解CSS:CSS逻辑属性》一文。

另外 CSS 盒模型中的 paddingborder-width 的值也将会影响一个容器的大小,同时 CSS 的 box-sizing 属性也会影响容器大小的计算:

容器空间

容器的空间实际上指的就是容器的大小,它分为可用空间和不可用空间,我们用下图来描述容器的可用空间和不可用空间:

滚动容器

正如容器空间中给大家示意的图片,我们在构建布局的时候,容器中的内容有可能会溢出容器,有可能不会溢出容器。但在容器中显式的设置 overflow 的值为 autoscroll 属性值时可以创建滚动容器。比如:

.container {
    overflow: auto;
    width: 50vw;
}

overflow 取值为 auto 时,只有当内容溢出容器时才会创建滚动容器;当 overflow 取值为 scroll 时,不管内容是否溢出容器都会创建滚动容器。比如下面这个示例,在容器 .container 中显式设置了 overflow-x: auto;,当内容溢出容器时,就创建了一个滚动容器:

滚动容器的轴线

滚动容器的轴表示滚动容器的滚动方向,它可以是水平或垂直的,x 值表示水平滚动,而 y 表示垂直滚动。CSS的 overflow 属性有两个子属性,分别对应的是 x 轴和 y轴:

  • 水平滚动,overflow-x 的值为 autoscroll
  • 垂直滚动,overflow-y 的值为 autoscroll

overflow-xoverflow-y 只是分别表示水平方向和垂直方向有可能会有滚动条出现,但并没有滚动轴线的概念。只在 CSS 滚动捕捉特性出现之后,才有了滚动容器的轴线,即使用 scroll-snap-type 属性指定的值,相当于指定了滚动容器的轴线:

/* 水平滚动轴线 */
.container-x {
    overflow-x: auto; /* 或 scroll*/
    scroll-snap-type: x;
}

/* 垂直滚动轴线 */
.container-y {
    overflow-y: auto; /* 或 scroll */
    scroll-snap-type: y;
}

我们还没开始介绍 scroll-snap-type 属性,不知道其具体的含义和作用并不重要,你只需要知道它是 CSS 滚动捕捉中的一个特性即可,后面将会详细介绍这个属性。

捕捉

使用过类似 AutoCAD 软件的同学应该有这样的印象,在屏幕中绘制图形时,移动一个对象时,对象总能自动吸附在栅格线上,使得对象只能落在栅格上的确定位置上,这就是 栅格捕捉。或者这样说,在一个普通的量尺上,规定你的画笔只能落在 1mm2mm的刻度上,而不能落在他们之间。

滚动捕捉

我们把在滚动时对滚动位置进行捕捉称为滚动捕捉。在 W3C 中通过两个示例向我们阐述了滚动捕捉的概念

先来看第一个示例:

<!-- HTML -->
<div class="photoGallery">
    <img src="img1.jpg" alt="" />
    <img src="img2.jpg" alt="" />
    <img src="img3.jpg" alt="" />
    <img src="img4.jpg" alt="" />
    <img src="img5.jpg" alt="" />
</div>

/* CSS */
.photoGallery {
    width: 500px;
    overflow-x: auto;

    /* 要求每次滚动的结束的位置精确地落在捕捉点上 */
    scroll-snap-type: x mandatory;
}

img {
    /* 指定每张图片的捕捉位置与滚动容器可视区域 x 轴中心的位置对齐 */
    scroll-snap-align: none center;
}

从示例的效果告诉我们,示例使用一系列图片水平排列在一个可滚动容器中,通过使用基于元素的位置捕捉,使得滚动结束时某个图片的位置将始终落在滚动视口(滚动容器可视区域)的中心位置。

正如上图所示,图中的红色区域即为可滚动容器的可视区域(也称捕捉视口),图片黄色框的地方被黎为捕捉区域。我们在图片元素 img(黄色框)显式设置了 scroll-snap-align: none center; 指定了横轴捕捉点为中心位置(图像的中心位置)。此时将在捕捉视口区域中心(红色虚线)以及捕捉区域中心(黄色虚线)形成捕捉点。

把上面的示例稍做调整,换成垂直滚动:

<!-- HTML -->
<div class="photoGallery">
    <img src="https://picsum.photos/500/200?random=1" alt="" />
    <img src="https://picsum.photos/500/300?random=2" alt="" />
    <img src="https://picsum.photos/500/400?random=3" alt="" />
    <img src="https://picsum.photos/500/500?random=4" alt="" />
    <img src="https://picsum.photos/500/400?random=5" alt="" />
    <img src="https://picsum.photos/500/300?random=6" alt="" />
    <img src="https://picsum.photos/500/200?random=7" alt="" />
</div>

/* CSS */
.photoGallery {
    overflow-y: auto;

    /* 使用非精确捕捉,能允许滚动最终停止在捕捉点的附近而不需要进一步的调整 */
    scroll-snap-type: y proximity;

    /* 指定捕捉视口应有 100px 的内距 */
    scroll-padding: 100px 0 0;
}

img {
    /* 指定每张图像的顶部为捕捉的位置 */
    scroll-snap-align: start none;
}

在本例中,将每张图像的滚动位置捕捉在每张图像靠近滚动容器的地方。这种非精确的捕捉能够让上一张图像的末尾出现在捕捉点(容器边缘)的附近,让用户能够感知到还没有到达所有图片的最顶部的效果。并且使用非精确的捕捉能够让用户在滚动中途随时停止,而不会像精确捕捉一样会强制将滚动位置修正到捕捉点上。然而在使用非精确捕捉时,如果滚动结束点已经位于捕捉点的附近,浏览器将会把最终的滚动点修改为指定的捕捉点上。

正如上图所示,首先是确定捕捉视口,由于使用了 scroll-padding 我们的捕捉视口与滚动可视区域有一个上边距为 100px 的距离,通过使用 scroll-snap-align 确定了捕捉区域的捕捉点位于区域的纵轴开始的位置。

滚动捕捉属性

CSS 滚动捕捉相关的属性和 CSS 的 Flexbox,Grid 属性类似,分为 作用于容器(滚动容器)属性作用于定位子项(滚动容器子元素)属性。其中作用于滚动容器的属性主要有 scroll-snap-typescroll-paddingscroll-snap-stop ;作用于定位子项的属性主要有 scroll-marginscroll-snap-align

scroll-snap-type

scroll-snap-type 属性是 CSS 滚动捕捉规范中最重要的一个属性,正如前面所介绍的一样,在滚动容器上显式设置了该属性才能被称之为滚动捕捉容器。它定义在滚动容器中的一个临时点(捕捉点)如何执行。简单地说,它定义了滚动捕捉的方向和执行的严格程度。其语法如下:

scroll-snap-type: none | [ x | y | block | inline | both ] [ mandatory | proximity ]?

其中 none 是其初始值。从语法规则上,scroll-snap-type 取值主要分为两个部分(非none值):

前半部分指定滚动容器轴线,用于指定滚动方向:

  • x :即 scroll-snap-type: x,表示滚动容器只捕捉其水平轴上(x轴)的捕捉位置
  • y :即 scroll-snap-type: y,表示滚动容器只捕捉其垂直轴上(y轴)的捕捉位置
  • block :即 scroll-snap-type: block,表示滚动容器只捕捉其块轴(Block Axis)的捕捉位置
  • inline :即 scroll-snap-type: inline,表示滚动容器只捕捉其内联轴(Inline Axis)的捕捉位置
  • both :即 scroll-snap-type: both,表示滚动容器会独立捕捉到其两个轴上的捕捉位置(可能会捕捉到每个轴上的不同元素)

注意,blockinlineCSS 逻辑属性中提出的概念,它和 CSS 的书写方式有关,因此上图中你会看到 blockinline 会在不同的方向,因为它们会随着 CSS 的 writing-modedirection 和 HTML 的 dir 属性变化。

scroll-snap-type的后部分取值主要用来定义滚动容器中滚动捕捉执行的严格程度:

  • mandatory :即 scroll-snap-type: mandatory,表示强制定位。如果它当前没有被滚动,这个滚动容器的可视窗口将静止在临时点(捕捉点)上。意思就是当滚动动作结束,如果可能,它会临时在那个点上。如果内容被添加、移动、删除或者重置大小,滚动偏移将被调整为保持静止在临时点(捕捉点)上。
  • proximity :即 scroll-snap-type: proximity,表示可能定位。如果它当前没有被滚动,这个滚动容器的可视窗口将基于用户代理滚动的参数去到临时点(捕捉点)上。如果内容被添加、移动、删除或者重置大小,滚动偏移将被调整为保持静止在临时点(捕捉点)上。

上面的示例,两个滚动容器的滚动轴都在 x 轴方向(水平滚动),不同的是左侧滚动捕捉是强制定位(mandatory),右侧滚动捕捉是可能定位(proximity),并且滚动对齐和滚动容器的开始处对齐(scroll-snap-align: start):

.mandatory {
    scroll-snap-type: x mandatory;
    overflow-x: auto;
}

.proximity {
    scroll-snap-type: x proximity;
    overflow-x: auto;
}

img {
    scroll-snap-align: start;
}

简单地来说,如果 scroll-snap-type 取值为 mandatory 时,项目(比如此例中的img)越过自身宽度的51%xinlie 轴)或高度的51%yblock轴),项目会继承向前滚动;而 scroll-snap-type 取值为 proximity 时,项目几乎要越过自身宽度或高度,项目才会继承向前滚动。

比如 scroll-snap-type 取值为 mandatory 的录屏效果,每次用户向右滚动时,浏览器都会将项目捕捉到容器的开头,而且当图片越过自身宽度 51% 时会继承向前滚动,反之会向后回退:

按理说,scroll-snap-type 取值为 proximity 时,项目几乎要超过自身宽度才会继续向前滚动,否则会回退,但从示例的结果来看,他又几乎和 mandatory 的效果几乎相似,至以为什么会这样,我也还没有搞清楚:

实质上按规范的阐述来说,两者是有区别的,比如下图所展示效果:

另外,scroll-snap-type 取值为 mandatory 能给用户提供更一致的用户体验。但是,规范中也指出,在遇到内容元素比滚动容器还高的情况,使用这个值就有点危险。

.container {
    height: 100vh;
    width: 40vw;

    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    scroll-padding: 10px;
    scroll-snap-type: y mandatory;
}

img {
    height: 120vh;
    width: auto;

    scroll-snap-align: start;
}

你可以尝试着体验一下上面的示例,虽然显式在容器 .container 显式设置了 scroll-snap-type: y mandatory;,但体验并不像前面介绍的那么好:

它总是会吸附到元素的顶部或下面元素的顶部,使得这个超高元素的中间部分内容是难完整查看。

scroll-padding

scroll-padding 属性用来指定滚动项目距离滚动容器的偏移量,这些偏移量定义了滚动视口的最佳浏览区域,即 作为目标区域的区域,用于将东西放置在用户的视野中。简单地说,该属性主要用于滚动容器上,用于设置所有侧面的滚动距离。其语法如下:

scroll-padding: [ auto | <length-percentage> ]{1,4}

scroll-padding 属性类似于 padding 的工作方式。它可以按方向拆分成多个属性:

我们来看一个简单的示例:

.container {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x mandatory;
    padding: 20px;
}

img {
    scroll-snap-align: start;
}

section:nth-child(2) .container {
    scroll-padding-left: 20px;
}

虽然在滚动容器中显式设置了padding: 20px,但可以明显看到scroll-padding带来的差异。右侧的示例,滚动容器的左侧有 20px的内距(scroll-padding-left: 20px)。结果滚动项目(img)将从左侧边缘捕捉到20px

左侧示例未设置 scroll-padding-left: 20px,右侧示例显式设置了 scroll-padding-left: 20px

这个属性在某些场景特别的有用,允许开发者排除被其他内容(如固定位置的工具栏或侧边栏)遮挡的滚动区域,或者只是在滚动项目和滚动容器边缘之间留出更多的空间。

scroll-snap-stop

在一个滚动容器中按预定方向滚动时,滚动容器可以“通过”几个可能的滚动捕捉位置(如果滚动操作使用相同的方向,但距离较小,则可以有效地捕捉到这些位置),然后到达滚动操作的自然终点并选择其最终滚动位置。通俗的说,滚动太快可能会跳过多个滚动项目,如下图所示:

可能需要一种方法来防止用户在滚动时意外跳过一些重要的滚动项,其中 scroll-snap-stop 就是我们需要的方法。换句话说,scroll-snap-stop 属性允许这样一个可能的捕捉公位置来“捕捉”滚动操作,迫使滚动容器在滚动操作自然结束之前停止。

scroll-snap-stop 属性可接受两个值:

  • normal : 滚动容器在执行滚动操作时,可能会经过该元素定义的捕捉点
  • always : 在执行滚动操作的过程中,滚动容器不能越过这个元素定义的捕捉点;而是必须捕捉到这个元素的第一个捕捉点

其中 normal 是默认值,如果要强制滚动捕捉到每个可能的点,应该使用 always 值。这样,用户可以一次滚动一个捕捉点,这种方式有助于避免跳过重要内容。

这个属性很有用,可以保证我们每次只滚动一个滚动项,而不会一下子滚动多个滚动项。

img {
    scroll-snap-align: start;
    scroll-snap-stop: var(--stop);
}

尝试切换 scroll-snap-stop 的值,你将看到取值 normalalways 的差异,比如下图的效果:

而且我们使用 scroll-snap-stop: always 实现一些特殊的效果,比如下面这个示例,每次向前(或回退时)滚动两张图片:

.container {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scroll-padding: 20px;
    scroll-snap-type: x mandatory;
}

img {
    scroll-snap-align: start;
}

img:nth-of-type(2n + 1) {
    scroll-snap-stop: always;
}

上例示例每次滚动,就只会滚动两张,因为在第135 (即2n + 1) 的滚动项上显示设置了scroll-snap-stop: always,正如前面所说的,滚动容器每次捕捉到指定滚动项的捕捉点。效果如下图所示:

scroll-snap-align

scroll-snap-align 是作用在滚动项目(滚动容器子元素)上的,用来指定捕捉点的位置,即捕捉点是在滚动项目的起点、中间还是结束点。该属性接受的值有:

  • none :默认值。不指定捕捉点位置
  • start :起始位置对齐,例如水平滚动,滚动项目的左侧边缘和滚动容器的左侧边缘对齐
  • center :居中对齐,例如水平滚动,滚动项目的水平中心位置和滚动容器的水平中心位置一致
  • end :结束位置对齐,例如水平滚动,滚动项目的右侧边缘和滚动容器的右侧边缘对齐

其中 startend 根据滚动容器的书写模式来解决,除非滚动捕捉区域大于捕捉视口,在这种情况之下,它们是根据盒子本身的书写模式来解决。还是同过示例来帮助大家理解 scroll-snap-align

上面示例是水平滚动效果,书写模式是LTR

如果上图不易于理解的话,可以看下面这个录屏的效果:

把上面的示例换成垂直滚动:

效果如下:

如果上图不易于理解的话,可以看下面这个录屏的效果:

需要特别注意的是,scroll-snap-align 还支持同时使用两个属性值,比如:

scroll-snap-align: start center;

如果 scroll-snap-align 只显式设置一个值的话,那么第二个值和第一个值相同:

scroll-snap-align: start

/* 等同于 */
scroll-snap-align: start start

scroll-margin

scroll-margin 属性主要用来设置滚动容器的子项(滚动项目)距离滚动容器的间距,和前面介绍的 scroll-padding 类似,而且也是一个简写属性,同样有物理属性和逻辑属性:

scroll-marginscroll-padding 不同的是,该属性作用于滚动项目上,而 scroll-padding 作用于滚动容器上。

来看一个示例:

.container {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x mandatory;
}

img {
    scroll-snap-align: start;
    scroll-snap-stop: always;
}

section:nth-child(2) img {
    scroll-margin: 20px;
}

示例中左侧效果是未使用 scroll-margin,右侧效果是使用了 scroll-margin,下图展示了它们之间的差异:

你也可以尝试着在上面的示例中滚动相应的图片,会看到对应的效果。

CSS 滚动捕捉的滚动事件停止及元素位置检测

首先声明,这部分的内容是参阅 @张鑫旭 老师的 《CSS scroll-snap滚动事件停止及元素位置检测 》一文而来。

虽然我在整理 CSS 滚动捕捉方面的知识,但开始并没有思考过 @张鑫旭 老师文章提到的需求,即 “滚动捕捉结束之后,需要高亮当前滚动项”。然而这种需求在未来真正的使用中或许是必不可少的,因此将文章提到的滚动中止检测方法纳入进来。具体的代码如下面这个示例所示:

实际上,标准制定者们和浏览器厂商正在积极推进滚动捕捉相关的 JavaScript API,以后要处理示例这样的效果,就会简单地多,而且随着这些 API 得到支持之后,CSS 滚动捕捉的功能会越来越强大。开发者也能更好的给用户提供更好的体验。