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;
}