滚动条的两个工程细节

740 字 · 2 分钟#CSS

滚动条大概是网页里最容易被忽视的控件:默认情况下它归操作系统管,样式不归你,宽度不归你,什么时候出现也不归你。但在追求体验一致性的项目里,这三件事都得管起来。这里记录两个我在实际需求中沉淀下来的细节。

防止滚动条出现导致的布局偏移

不同系统对浏览器滚动条的处理方式不一样:macOS 默认是悬浮式滚动条,不占布局宽度;Windows 的经典滚动条则实打实地占据十几像素。为了统一表现,通常会用 CSS 自定义滚动条:

::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-thumb {
  border-color: transparent;
  border-style: solid;
  border-width: 2px;
  border-radius: 100px;
  background: rgba(73, 79, 87, 0.2);
  background-clip: padding-box;
}

自定义之后滚动条就会占据网页的实际宽度,新的问题随之而来:内容高度是动态的(比如异步加载的列表),滚动条时有时无,出现的瞬间会把内容挤窄一截——一次肉眼可见的布局偏移。

这时 overflow 的两个值行为完全不同:

  • overflow: auto 不会预留滚动条宽度,滚动条出现时挤压内容,造成前后表现不一致;
  • overflow: scroll 总是预留滚动条的宽度,子元素高度不够时也预留,代价是内容不足时会留出一条什么都没有的空槽。

scroll 用预留空间换来了稳定,这在动态列表场景里通常是值得的交易。而如果不需要兼容老浏览器,现在有了更体面的答案——scrollbar-gutter

.list {
  overflow: auto;
  scrollbar-gutter: stable;
}

它把「预留滚动条的空槽」和「是否显示滚动条」解耦了:空间始终预留,滚动条按需出现,布局从头到尾稳定。

亲手试试——左边的列表在滚动条出现时会被挤窄一截,右边的纹丝不动:

overflow: auto
  • 1 条 · 注意我的宽度
  • 2 条 · 注意我的宽度
  • 3 条 · 注意我的宽度
overflow: auto + scrollbar-gutter: stable
  • 1 条 · 注意我的宽度
  • 2 条 · 注意我的宽度
  • 3 条 · 注意我的宽度

文档:overflow - MDN

把子元素的滚动条伪装成父元素的滚动条

另一个场景:设计稿上滚动条贴着容器的最右边,但真正滚动的是容器里有内边距的子元素——滚动条出现在内边距以内,和设计稿差了一截。

利用负外边距和内边距的组合,可以把子元素的滚动条「推」到父元素的边缘上:

.child {
  margin: 0 -24px;
  padding: 0 24px;
}

24px 对应父元素的内边距:负外边距让子元素撑满到父元素边缘(滚动条因此贴边),等值的内边距再把内容顶回原来的视觉位置。实际取值时还要把子元素自身的内边距和滚动条的宽度算进去。

两个容器的结构完全相同,只差这一组 margin/padding:

未处理:滚动条陷在 padding 里

1 行 · 滚动条应该贴住父容器的右缘

2 行 · 滚动条应该贴住父容器的右缘

3 行 · 滚动条应该贴住父容器的右缘

4 行 · 滚动条应该贴住父容器的右缘

5 行 · 滚动条应该贴住父容器的右缘

6 行 · 滚动条应该贴住父容器的右缘

7 行 · 滚动条应该贴住父容器的右缘

8 行 · 滚动条应该贴住父容器的右缘

margin: 0 -24px + padding: 0 24px

1 行 · 滚动条应该贴住父容器的右缘

2 行 · 滚动条应该贴住父容器的右缘

3 行 · 滚动条应该贴住父容器的右缘

4 行 · 滚动条应该贴住父容器的右缘

5 行 · 滚动条应该贴住父容器的右缘

6 行 · 滚动条应该贴住父容器的右缘

7 行 · 滚动条应该贴住父容器的右缘

8 行 · 滚动条应该贴住父容器的右缘

写在最后

这两个技巧解决的其实是同一类问题:滚动条是布局的一部分,却常常被当作布局之外的东西来对待。把它纳入布局的账本里——预留它的宽度、安排它的位置——体验的最后一公里才算跑完。