大多数 Web 页面不适合单屏显示,所以 Web 页面出现滚动条,被所有用户认为是理所当然的。对于 Web 开发者来说,跨浏览器提供良好的滚动体验,同时符合设计,无疑是一个挑战。尽管 Web 标准的发展速度远超从前,但代码的实现往往是落后的。
这节课将为你介绍一些可用于优化和改善用户滚动体验的 CSS 特性,使用这些特性可以使你的 Web 页面滚动更平滑、美观且性能更好。在此基础上,你可以检查一下你所用的解决方案是否能使用更优雅的方案所代替。
为什么需要滚动?
正如你所看到的,滚动在 Web 页面上随处可见,可以在页面中滚动,也可以在个别容器中滚动。它可以是垂直方向的滚动,也可以是水平方向的滚动:
这一切对于大家来说,既是那么的熟悉,又是那么的陌生。有同学可能会问,为什么需要滚动呢?又何时会出现滚动呢?
我们回到 CSS 的世界中来。
稍微对 CSS 有所了解的同学都知道,在 CSS 中,万物皆是一个盒子,而且盒子是有大小的,有可能没有足够多的空间来容纳其内容,CSS 把这种现象称为内容溢出 :
上图中,左侧内容超出了容器,产生了内容溢出;右侧因内容少,未产生内容溢出。
默认情况下,内容溢出盒子是可见的,但你可以(如果愿意)以不同的方式来管理溢出的内容。例如,在容器上设置 overflow
属性的值为 auto
或 scroll
,它会为你的容器提供滚动条。
这可能不是你想要的(你不希望容器出现滚动条),但是在某些设计模式中,滚动容器是合适的。而且,CSS 选择默认使溢出内容可见(而不是隐藏),也是 CSS 设计的核心所在。
因为,在 CSS 中(和大多数地方一样),我们应该尽量避免数据丢失(通常说的是内容的丢失)。例如,你给容器的 overflow
指定一个 hidden
或 clip
值(或者使用了其他的 CSS 裁剪术)时,溢出容器的内容就会消失(视觉上的消失)。这就意味着,用户可能会错过一些内容,甚至是重要的信息。
在某些情况下,内容消失会产生更大的、真正的问题。如果你开发的 Web 应用是脆弱的,比如,未考虑更多的场景或适配性,以至于表单的“提交按钮”位于容器的裁剪区域(填写表单的用户看不到这个按钮),这样就会使你的用户无法提交已填写的表单:
如果页面因内容溢出打破了整个布局,你会发现它。或者,在更糟糕的情况下,使用该网站的人会发现它并让你知道。
比较严重的是,事情的消失并不总是那么明显。作为Web 开发者或设计师,你可能没有发现这个问题,特别是当它只发生在响应式 Web 设计中的特定视窗大小时(如上图所示)。你的用户可能没有发现问题,他们只是没有看到“提交按钮”(CTA),或者认为这是他们的问题,他们不能正常提交表单,所以离开了。
正因如此,为了避免因数据的丢失(内容视觉上不可见)造成不必要的损失,Web 页面或容器需要滚动,除非是你不希望滚动条出现。
如何创建滚动?
CSS 中关于滚动有一个专业术语,称为可滚动溢出(Scrollable Overflow) ,在 W3C 的溢出模块中有明确的定义。简单地说,它是用来创建滚动容器的。
如果你希望某个容器变成一个滚动容器,那么它需要同时具备以下几个条件:
- 容器的尺寸受到上下文的限制或者你给容器指定了一个具体的尺寸;
- 容器的内容溢出,即内容的尺寸大于已指定的容器尺寸;
- 容器的
overflow
属性的值是一个非visible
值。
虽然 CSS 的 overflow
属性是用来控制元素内容溢出的方式,但它会告诉浏览器你想怎么处理溢出。即,指定了一个盒子的内容是否被裁剪到它的内距盒子框(<padding-box>
)的边缘,如果是的话,它是否是一个滚动容器,允许用户将其可滚动溢出区域的剪切部分滚动到滚动容器的视窗中。
滚动容器视觉视窗(可见区域)与它的内距盒子框(
<padding-box>
)边缘重合,被称为滚动视窗 。
也就是说,一个元素是否是一个滚动容器,是由 overflow
(或它的子属性 overflow-x
和 overflow-y
****)的值来决定的 。
overflow
可接受的值有 visible
(默认值)、hidden
、auto
、scroll
和 clip
,不同值在浏览器中的表现如下图所示:
只有元素的 overflow
属性的值为 hidden
、auto
或 scroll
时,该元素才是一个滚动容器。只不过 hidden
和 scroll
与 auto
有所不同。
scroll
会使滚动容器始终出现滚动条;auto
只有内容溢出时才会使滚动容器出现滚动条;hidden
始终不会让滚动容器出现滚动条,并且会对溢出容器的内容进行剪切(溢出容器盒子的内容不可见),同时容器也不会出现滚动条(用户无法滚动),但可以使用Element.scrollTop
进行滚动。
当然,如果你希望控制一个元素溢出的方式能和书写模式紧密结合在一起,那么你可以使用 overflow-x
和 overflow-y
对应的 CSS 逻辑属性,即 overflow-inline
和 overflow-block
。这两个属性都可以分别映射到 overflow-x
和 overflow-y
,至于映射到哪个上面,取决于文档的书写模式:
- 如果文档的书写模式是
ltr
或rtl
(即horizontal-tb
),那么overflow-inline
映射到overflow-x
上,overflow-block
映射到overflow-y
上; - 如果文档的书写模式是
vertical-lr
或vertical-rl
,那么overflow-inline
映射到overflow-y
上,overflow-block
映射到overflow-x
上。
需要注意的是,滚动容器的滚动条位置会因文档书写模式进行调整:
如果你不想思考过多,那么你只需要记住,在容器元素上显式设置 overflow
属性的值为 auto
或 scroll
时,相应的就创建了滚动,只不过 auto
只有内容溢出容器时才会出现滚动条。
滚动容器给 Web 带来哪些变化?
滚动容器给 Web 带来最直观的变化就是容器出现滚动条。滚动条的出现给 Web UI 和 Web 布局带来直接的影响。这主要是因为滚动条的类型、 UI 效果和尺寸大小和系统平台密不可分。
先来看滚动条的类型,它主分为经典型滚动条和覆盖式滚动条:
上图左侧是覆盖式滚动条 ,在 iOS 或 Mac 系统上很常见,它被放置在内容之上。它们默认不显示,只在用户滚动的时候显示。为了保持滚动条下面的内容可见,它们都是半透明的,但这完全由用户代理(浏览器)来决定。在与它们互动时,其外观(包括大小)会有所不同。
右侧的是经典型滚动条 ,在 Windows 系统上很常见,它总是放置在一沟槽中(也称“滚动沟槽”),位于边框盒子(<border-box>
)内边缘和内距盒子(<padding-box>
)外边缘之间。当出现时占用空间,通常是不透明的,会从相邻的内容中拿走一些空间(改变盒模型大小)。
采用经典型滚动条,滚动条的出现会引起布局变化,产生重排和重绘,对于渲染性能来说是昂贵的。这是因为,滚动条的存在可能影响盒子尺寸的情况,所以 UA 必须从假设不需要滚动条开始,如果发现需要滚动条,则重新计算盒子尺寸。
在 macOS 系统中,用户可以根据自己的喜好来设置滚动条的类型:
另外,不同系统下,滚动条的尺寸(一般指的是滚动沟槽)的大小也不一致,因而获取它的宽度并不容易。在 macOS 系统中,无论任何浏览器(滚动条)都是统一 15px
,然而 Windows 系统可能会令开发者发狂:
除此之外,滚动条的 UI (外观)也会因系统不同,甚至同一系统在不同时间也有所差异,尤其是 Windows 系统中:
所以自定义滚动条 UI 的呼声越来越高:
有关于自定义滚动条的相关介绍,将放到小册的下一节课中与大家一起探讨!
滚动条的出现,除了在 UI 和布局上给 Web 带来变化之外,给用户体验也带来不少的变化。比如滚动穿透 、下拉刷新 和滚动卡顿等。就拿滚动穿透为例:
不难发现,模态框弹出之后,页面上有两个滚动条出现,一个是模态框的,另一个是页面(body
)的。按理说,在模态框滚动时,它滚动到模态框底部时,溢出容器(模态框)应该停止滚动,因为没有更多内容要消费。换句话说,用户到了一个“滚动边界”。
但你仔细观察,如果用户继续滚动时,虽然模态框的滚动条滚动到底部无法继续滚动,但位于模态框底部的页面滚动条却依然在继续滚动,直到页面没有内容可消费时,滚动才会停止。
这种行为被称为滚动穿透,也是浏览器滚动内容的默认行为。通常情况下,默认行为是很好的,但有时它是不可取的,甚至是意想不到的。某些应用可能希望在用户触碰到滚动边界时提供不同的用户体验。
再来看下拉刷新。下拉刷新在移动应用中已经是很常见的一种交互效果了,但这也给用户带来一些不好的体验。比如用户滚动页面时,很容易就触发了下拉刷新:
虽然滚动是 Web 中必不可少的一种交互行为,但也给 Web 页面性能带来极大的影响。最为简单的,比如,你在做全屏滚动时,默认之下,它会一闪而过,或者闪过之时略带卡顿:
事实上,你期望给用户的是一个丝滑般的滚动体验:
以往要实现这种滚动效果,我们不得不依赖于 JavaScript 脚本或其他第三方 JavaScript 库。庆幸的是,我们使用 CSS 可以解决上面所提到的各种问题,这也是我们这节课的重点部分。
改善用户体验的滚动新特性
CSS 特性随着时代的变更,也在不断的向前演进,这些新特性使 Web 开发者在实现一些 Web 效果不再需要依赖 JavaScript 脚本。这些 CSS 新特性中就包括了用来优化滚动体验相关的特性。
为滚动条保留空间
前面提到过,不同系统不同时间下,放置滚动条的空间大小(滚动沟槽)是不一样的。在经典型滚动条的状态下,很易于让 Web 产生回流(重排和重绘),尤其是滚动条出现之时,容器为了放置滚动条,会在滚动容器的边框盒子与内距盒子之间产生一个空间。此时,Web 页面布局就会产生变化,即产生回流:
现在,你可以使用 CSS 的 scrollbar-gutter
属性为滚动条提前预留空间,避免页面布局的变化。
注意,CSS 滚动沟槽只存在于经典型滚动条状态之下,如果你的操作系统是 macOS 系列,则需要将系统中“通用(General)”设置中的“显示滚动条(Show scroll bars)”选项设置为“始终(Always)”,才可以将覆盖式滚动条更换成经典型滚动条,就可以在浏览器中查看到
scrollbar-gutter
的效果。
scrollbar-gutter
属性可接受的值有 auto
、 stable
和 both-edges
,而且该属性需要和 overflow
属性结合在一起使用。scrollbar-gutter
同一个值与 overflow
属性不同值时,滚动条保留的空间也是有所差异的。
auto
:当overflow
为scroll
或auto
且有内容溢出时,经典型的滚动条会通过创建滚动沟槽来占用盒子(滚动容器)空间。stable
:当overflow
为hidden
、scroll
或auto
时,经典型滚动条会出现滚动沟槽(不管内容是否溢出容器)。stable both-edges
:它是stable
的一个扩展值。可以通过both-edges
实现对称性,即滚动容器两边都有滚动沟槽等同的空间。
分别来看它们的组合效果:
.overflow {
overflow: auto;
scrollbar-gutter: var(--scrollbar-gutter);
}
把示例中的 overflow
的值换成 scroll
:
注意,overflow
取值为 hidden
时也能创建滚动容器,只是内容被裁剪,滚动条不会出现。但它和 scrollbar-gutter
不同值结合在一起时,效果如下:
你会发现,scrollbar-gutter
取值为 stable
时,不会改变滚动条本身是否可见,只影响到沟槽的存在:
也就是说,在滚动容器上指 scrollbar-gutter
的值为 stable
,可以让 UA 始终显示滚动沟槽,即使内容没有溢出滚动容器,也没有显示滚动条。这样我们就有了一个视觉上稳定的布局:“当内容开始溢出容器时,滚动条就会显示出来,但不会发生布局移动,因为它会告诉浏览器,要给滚动条预留一定空间(滚动沟槽)”。
另外,当滚动沟槽存在而滚动条不存在时,滚动沟槽的背景会作为 padding
的延伸被绘制出来。
最后我们可以用下面这个综合示例来展示 scrollbar-gutter
和 overflow
的交互作用,示例在浏览器中呈现的效果能更好地帮助你理解 scrollbar-gutter
是如何给滚动条预留空间的:
下图阐述经典型滚动条预留空间(滚动沟槽)是否应该存在:
不过,使用 scrollbar-gutter
有两个注意事项:
- 像
overflow
属性一样,根元素(<html>
)上设置的scrollbar-gutter
会被应用到视窗中; - 与
overflow
属性不同的是,浏览器不会从 HTML 的<body>
元素中传播scrollbar-gutter
。
也就是说,如果你能预测容器是一个滚动容器,它有可能会出现滚动条,那么应该在滚动容器上设置 scrollbar-gutter
为 stable
:
.scroll--container {
scrollbar-gutter: stable;
}
在必要时显示滚动条
虽然说 scrollbar-gutter
可以给滚动条保留预定的空间,避免页面的回流,但我们还是应该尽可能地在必要时显示滚动条 。
因此,在内容比较长的情况下,更推荐将 overflow
的值设置为 auto
。如果你将 overflow
显式设置为 scroll
时,不管容器内容长短,滚动条都会像下图这样展示出来:
这种效果并不友好,在非必要的情况下,滚动条不应该向用户展示。只需要在滚动容器中显式设置 overflow
为auto
即可改变这种现象:
.element {
overflow-y: auto;
}
容器设置 overflow-y
为 auto
时,只有内容过长溢出滚动容器时,滚动条才会向用户展示,内容不溢出容器,则不会展示滚动条:
阻止滚动穿透下拉刷新
带有滚动的模态框是滚动穿透的典型案例之一,模态框内容溢出时会出现滚动条。此时,Web 页面就会有两个滚动条出现,一个是模态框(子滚动容器,也被称为隐式滚动器),另一个是 body
(称为根滚动器,每个 Web 页面只有一个根滚动器)。浏览器针对此场景时,其默认的滚动行为如下所示:
当模态框内容滚动到模态框底部时,模态框内容所在的滚动器(隐式滚动器)没有溢出可消费的内容,根滚动器(body
的滚动器)会开始滚动:
浏览器这种默认行为通常是不需要的,并且会分散用户对模态框内容的注意力。以往为了改变这种行为,一般是借助 JavaScript 来完成。当模态框出现时,给 html
或 body
元素添加一个类名(比如 .modal--open
),反之移除这个类名。然后在 .modal--open
类名上设置 overflow: hidden
:
.modal--open {
overflow: hidden;
}
不过,上面这段 CSS 代码在 iOS 的 Safari 中并不能很好地工作。如果要避免这个现象,你需要在此基础上设置 position: fixed
:
.modal--open {
position: fixed;
overflow: hidden;
}
不幸的是,这又会引起新的问题出现,它会导致浏览器滚动到页面顶部时,分散用户手头任务的注意力。针对这一现象,@Ben Frain 早在 2016 年就提供了相应的解决方案。即,模态框弹出时,给 body
添加下面这段 CSS 代码:
.bg-scrolling-element-when-modal-active {
touch-action: none;
-webkit-overflow-scrolling: none;
overflow: hidden;
overscroll-behavior: none;
}
注意,该方案适用于 iOS 13+ 的系统,具体效果可以点击这里查阅。
如今,我们完全可以不依赖任何 JavaScript 脚本,仅使用 CSS 就可以阻止滚动穿透。W3C 的 CSS Overscroll Behavior Module Level 1 提供了一个名为 overscroll-behavior
的属性,让你可以控制浏览器过度滚动时的表现——也就是滚动到边界。
overscroll-behavior
属性和 overflow
相似,可以分别在 x
轴(overscroll-behavior-x
)和 y
轴方向(overscroll-behavior-y
)阻止滚动穿透。该属性提供了三个可选值,auto
、contain
和 none
,其中 auto
是其初始值,允许滚动穿透。
要是你给滚动容器指定 overscroll-behavior
属性的值为 contain
,默认的滚动边界行为不变(“滚动触底”或者刷新),但是可以阻止滚动穿透。比如,模态框滚动到底部时,位于其底下的 body
不会有滚动行为:
.modal--content {
overflow-y: auto;
overscroll-behavior-y: contain;
}
我录制了一张 GIF 图来向大家呈现 overscroll-behavior
值为 auto
和 contain
的差异:
除此之外,overscroll-behavior
还可以取 none
值,它除了可以帮助我们阻止滚动穿透之外,同时也会阻止滚动到边界时的反弹及刷新页面的效果,即移除滚动至顶部或底部的默认滚动特效(例如 Android 上的 Chrome 当滚动超过顶部边界时会刷新页面)。
body {
overscroll-behavior: none;
}
注意,如果要移除滚动至顶部或底部的默认滚动特效,需要在 html
或 body
元素上设置 overscroll-behavior
的值为 none
。这也是禁用原生下拉刷新最有效的方案,而且它对于我们定制一个下拉刷新是非常有利的。否则就会出现两个下拉刷新的效果(一个是原生的,一个是定制的)。
未使用 overscroll-behavior
之前的效果:
使用 overscroll-behavior
之后的效果:
详细代码可以查阅 @ebidel 在 Github 上提供的相关示例。
创建丝滑般的滚动
上图这种全屏滚动的效果在 Web 上很常见。我想你已经观察到了:
- 当
scroll-behavior
的值为auto
时,整屏的滚动一闪而过,没有任何平滑过渡效果; - 当
scroll-behavior
的值为smooth
时,整屏滚动时明显要更丝滑。
是的,这就是 CSS 中创建丝滑般滚动特效方案之一。
scroll-behavior
允许你控制滚动容器的一些特殊行为。它可以为一个滚动容器指定滚动行为,其他任何的滚动,例如那些由于用户行为而产生的滚动,不受这个属性的影响。另外,在根元素中指定这个属性时,它反而适用于视窗。
html {
scroll-behavior: smooth;
}
当 scroll-behavior
取值为 smooth
时,滚动框将通过用户代理预定义的时长、使用预定义的时间函数,来实现平滑的滚动,用户代理应遵循其平台的约定,如果有的话。
再来看另一个实用性的案例:
<body>
<header>Header Section</header>
<main>
<article><!-- 文章内容 --></article>
<div class="back-to-top-wrapper">
<a href="#top" class="back-to-top-link" aria-label="Scroll to Top">🔝</a>
</div>
</main>
</body>
html {
scroll-behavior: smooth;
}
.back-to-top-link {
position: sticky;
pointer-events: all;
top: calc(100vh - 5rem);
transition: transform 80ms ease-in;
}
.back-to-top-link:hover,
.back-to-top-link:focus {
transform: scale(1.1);
}
.back-to-top-link:focus {
outline: none;
box-shadow: 0 0 0 3px #4e85c0;
}
效果是不是很棒!
这里有一个小技巧,你可以在所有滚动容器中设置 scroll-behavior
的值为 smooth
,尤其是在 html
和 body
元素上,这样做,可以让你的 Web 页面上所有滚动带有丝滑般的效果。
在 CSS 中,除了 scroll-behavior
用来改变滚动效果之外,还有滚动捕捉(scroll-snap-*
)相关的特性。结合在一起,可以让你提供一个流式精确的滚动体验,有点类似于 Swiper 的幻灯片效果:
你甚至可以将 scroll-behavior
和滚动捕捉结合起来,让滚动效果更完美一些。比如下面这个示例:
<body>
<section><!-- 区域中内容 --></section>
<!-- 其他 section -->
</body>
html {
height: 100vh;
width: 100vw;
--scroll-behavior: auto;
scroll-behavior: var(--scroll-behavior);
scroll-snap-type: y mandatory;
}
section {
position: relative;
width: 100%;
height: 100vh;
scroll-snap-stop: always;
scroll-snap-align: center;
}
代码中 scroll-snap-*
相关的属性就是滚动捕捉中的特性,有的用于滚动容器,有的用于滚动容器中的项目。由于篇幅的原因,在这节课中不对它们进行详细的阐述,因为我们在后面的课程中有专门一节课和大家探讨滚动捕捉方面的知识,敬请期待……
小结
滚动在 Web 应用(或页面)上随处可见,不同系统和平台对容器滚动的行为和 UI 渲染都有所不同,这也造成美观上有所差异,比如滚动条的 UI 效果。除此之外,一些默认的滚动行为并不能给用户提供较好的体验。
庆幸的是,CSS 提供了很多新特性,可以帮助我们提高用户的体验,比如:
- 将
overflow
设置为auto
,使容器在必要时才出现滚动条; - 将
scrollbar-gutter
设置为stable
,可以给滚动条预留空间(如果有需要的话),可以避免滚动条出现时造成页面的回流,这对 Web 性能优化是很有帮助的; - 可以在滚动容器上设置
overscroll-behavior
的值为contain
或none
,阻止滚动穿透和下拉刷新; - 可以在滚动容器上设置
scroll-behavior
的值smooth
,让滚动效果更丝滑。
即,你可以在滚动容器上使用下面这段代码:
.scroll--container {
overflow: auto; /* 可以是 overflow-x 或 overflow-y */
scrollbar-gutter: stable; /* 如果有需要的就设置 */
overscroll-behavior: contain; /* 如果要阻止下拉刷新的话,请使用 none */
scroll-behavior: smooth; /* 提供丝滑般滚动效果 */
}
除此之外,我们还可以使用滚动捕捉为用户提供一个流式精确的滚动体验,还可以为滚动条定制个性化的 UI 。我将在后面的课程中与大家继续探讨这两个方面的知识。
最后,希望你能重新审阅你的代码,在滚动容器上加上课程中提到的 CSS 特性,为你的用户提供更好的体验。