CSS 滚动条与滚动行为

深入理解 CSS 滚动条的自定义方式、scrollbar-width 和 scrollbar-gutter 的实际用法、touch-action 的触摸行为控制、overscroll-behavior 的滚动穿透,以及 scroll-snap 的对齐机制。

CSS 滚动条与滚动行为

为什么滚动条样式是现代 UI 的细节体验

在互联网早期,所有浏览器的滚动条都是丑丑的灰色条。今天,现代 CSS 提供了自定义滚动条的强大能力,让滚动条能够融入设计系统。但除了样式自定义,还有滚动行为、滚动吸附、触摸行为等重要的用户体验相关属性。

理解这些属性不只是"把滚动条变成灰色还是白色"。你需要理解滚动容器的形成条件scrollbar-gutter 如何解决内容抖动、滚动吸附的实现机制、以及移动端触摸行为的特殊处理。这些细节决定了你的 UI 在各种设备和场景下的体验质量。

面试定位:滚动条样式在面试中不常单独出现,但涉及的性能优化(滚动流畅度)、用户体验(滚动吸附)、移动端适配(touch-action)等问题体现了前端工程师对细节的关注。


自定义滚动条

webkit-scrollbar(Chromium 系浏览器)

/* 整个滚动条 */
.custom-scrollbar {
  scrollbar-width: thin; /* Firefox */
  scrollbar-color: #888 #f1f1f1; /* Firefox */
}

/* webkit 滚动条各部分 */
.custom-scrollbar::-webkit-scrollbar {
  width: 8px;  /* 垂直滚动条宽度 */
  height: 8px; /* 水平滚动条高度 */
}

.custom-scrollbar::-webkit-scrollbar-track {
  background: #f1f1f1; /* 轨道背景 */
  border-radius: 4px;
}

.custom-scrollbar::-webkit-scrollbar-thumb {
  background: #888; /* 滑块颜色 */
  border-radius: 4px;
}

.custom-scrollbar::-webkit-scrollbar-thumb:hover {
  background: #555; /* hover 状态 */
}

.custom-scrollbar::-webkit-scrollbar-corner {
  background: #f1f1f1; /* 滚动条角落 */
}

scrollbar-width 和 scrollbar-color(标准属性)

/* Firefox 支持的标准属性 */
.standard-scrollbar {
  scrollbar-width: thin;    /* thin | normal | none */
  scrollbar-color: #888 #f1f1f1; /* thumb color track color */
}

scrollbar-gutter

scrollbar-gutter 控制滚动条是否占用空间,避免内容抖动:

/* 默认:滚动条占用空间时内容抖动 */
.default {
  overflow: auto;
}

/* 稳定模式:始终预留空间给滚动条 */
.stable {
  overflow: auto;
  scrollbar-gutter: stable;
}

/* 稳定+块状:只在需要时预留空间 */
.stable-block {
  overflow: auto;
  scrollbar-gutter: stable block;
}

/* 两者对比:
 * .default: 内容区宽度 = 可用宽度 - 滚动条宽度(滚动条出现时收缩)
 * .stable: 内容区宽度 = 可用宽度 - 滚动条宽度(始终保持)
 */

滚动吸附(scroll-snap)

scroll-snap-type

.container {
  scroll-snap-type: x mandatory;
  /* x: 水平滚动吸附
   * y: 垂直滚动吸附
   * both: 两个方向都吸附
   * mandatory: 滚动结束后必须停在吸附点
   * proximity: 滚动结束后吸附(如果靠近吸附点)
   */
}

scroll-snap-align

.item {
  scroll-snap-align: start;   /* 对齐到容器起始边缘 */
  scroll-snap-align: end;     /* 对齐到容器结束边缘 */
  scroll-snap-align: center;  /* 对齐到容器中心 */
  scroll-snap-align: none;    /* 不吸附 */
}

完整示例

.carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: 16px;
}

.carousel-item {
  flex: 0 0 300px;
  scroll-snap-align: start;
}

scroll-margin 和 scroll-padding

.item {
  scroll-snap-align: start;
  scroll-margin: 20px;  /* 吸附时的外边距 */
}

.container {
  scroll-snap-type: x mandatory;
  scroll-padding: 20px; /* 吸附容器的内边距 */
}

touch-action

touch-action 控制触摸设备上的手势行为:

/* 默认:浏览器处理所有触摸手势 */
.default {
  touch-action: auto;
}

/* 禁用双击缩放 */
.no-zoom {
  touch-action: manipulation;
}

/* 只允许水平和垂直滚动 */
.scroll-only {
  touch-action: pan-x pan-y;
}

/* 只允许水平滚动 */
.horizontal-only {
  touch-action: pan-x;
}

/* 只允许垂直滚动 */
.vertical-only {
  touch-action: pan-y;
}

/* 完全禁用触摸 */
.no-touch {
  touch-action: none;
}

常用场景

/* 地图组件:需要手势缩放 */
.map {
  touch-action: none; /* 自定义手势处理 */
}

/* 轮播图:只允许水平滑动 */
.carousel {
  touch-action: pan-x;
}

/* 全屏视频:禁用触摸 */
.video {
  touch-action: none;
}

overscroll-behavior

overscroll-behavior 控制滚动到边界时的行为:

/* 默认:滚动穿透(继续滚动父容器) */
.default {
  overscroll-behavior: auto;
}

/* 禁止滚动穿透 */
.no-bleed {
  overscroll-behavior: none;
}

/* 只禁止水平/垂直穿透 */
.no-bleed-x {
  overscroll-behavior: contain none;
}

.no-bleed-y {
  overscroll-behavior: none contain;
}

/* 场景:模态框内滚动时禁止穿透到页面 */
.modal {
  overflow: auto;
  overscroll-behavior: contain;
}

滚动驱动动画

scroll() 函数

@keyframes progress {
  from { width: 0; }
  to { width: 100%; }
}

.progress-bar {
  animation: progress linear;
  animation-timeline: scroll();
  /* 或指定滚动容器 */
  animation-timeline: scroll(root);
}

滚动容器查询

.container {
  scroll-timeline: --my-scroll;
}

@keyframes slide {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

.sliding-element {
  animation: slide linear;
  animation-timeline: --my-scroll;
}

实战场景

场景一:自定义美化滚动条

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

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: rgba(0, 0, 0, 0.2);
  border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(0, 0, 0, 0.4);
}

/* Firefox */
* {
  scrollbar-width: thin;
  scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}

场景二:水平轮播图的吸附效果

.swiper {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding: 20px;
  gap: 20px;
}

.swiper-slide {
  flex: 0 0 calc(100% - 40px);
  scroll-snap-align: center;
}

/* 移动端优化 */
@media (max-width: 768px) {
  .swiper-slide {
    flex: 0 0 85%;
  }
}

场景三:防止模态框滚动穿透

.modal-open {
  overflow: hidden;
}

.modal {
  position: fixed;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

延展阅读