你以为打开弹窗页面抖一下是 CSS 的 bug?今天这件事被一行 scrollbar-gutter 彻底修了
滚动条凭什么能把你抖出去
这个问题从根上讲很简单。桌面浏览器里,滚动条是占用物理空间的,不是什么浮层。内容多了,滚动条出现,viewport 可用宽度少掉 15-17px。内容少了,滚动条消失,可用宽度又回来了。
如果这个过程中,页面上有 margin: 0 auto 居中的容器,它的宽度是固定的——少掉的那 15px,浏览器会重新计算边距,整个内容区往左移。弹窗关了,边距又算回去,一切复原。打开,关上,再打开,再关上。
每抖动一次,用户会感到那一下——即使他们说不清是什么在动。开发者呢,要么靠眼睛盯着,要么等 QA 报 bug。
固定定位的元素更惨。right: 0 的固定导航栏,本来贴着右边缘,结果父容器宽度因为滚动条缩了,它也跟着往左挪。弹窗打开,导航栏抖一下。弹窗关了,导航栏又抖回来。「这个 fixed 怎么这么难调」,大多数时候不是 fixed 难调,是滚动条在背后偷走了那几像素。
你的团队是怎么「修」这个的
我见过的方案大概分两种。
第一种是量出来打补丁。打开弹窗前,用 JS 算一次滚动条宽度:
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
document.body.style.paddingRight = `${scrollbarWidth}px`;
document.body.style.overflow = 'hidden';
弹窗关了,再把 padding 清掉。
这个方案能 work。但它有几个问题:
- 每次新建弹窗、抽屉、下拉菜单、搜索浮层,都要写这段逻辑,或者抽成一个
lockScroll()函数 - 实际项目中,这个函数最后会出现在 3-5 个不同的地方
- 每个地方复制粘贴之后,过几个月,其中一处的计算逻辑悄悄改掉了,另外几处没跟上
- 打开弹窗到 padding 生效之间,总有那么一帧——滚动条已经消失,padding 还没加上——用户会看到那一下抖动
第二种是用 100vw 硬算:
html {
margin-left: calc(100vw - 100%);
}
100vw 包含滚动条宽度,100% 不包含,两者的差值就是滚动条的宽度。这个方案比 JS 稳定,但它是给 html 元素全局加 margin,副作用是会影响固定定位元素的对齐,需要在每个 fixed 元素上单独补偿。
两件事本质一样:CSS 没有给滚动条预留空间的能力,开发者只能靠后天补丁来模拟这个行为。
scrollbar-gutter: stable,一行修掉
scrollbar-gutter 是 CSS Scrollbars Module Level 1 引入的属性,专门用来控制滚动条 gutter(滚动条槽)的预留行为。核心取值就两个:
html {
scrollbar-gutter: stable;
}
stable 的语义是:无论内容是否溢出、滚动条是否实际出现,浏览器都为滚动条保留固定的空间。当 overflow: hidden 锁住滚动时,gutter 空间依然保留,内容宽度不变,布局不抖。
这对弹窗场景来说,意味着:
- 弹窗打开 →
overflow: hidden→ 滚动条消失 → gutter 空间保留在原地 → 内容宽度不变 → 不抖 - 弹窗关闭 →
overflow恢复 → gutter 保持 → 内容宽度不变 → 不抖
整个过程,layout 眼里滚动条槽从来没变过,所以不需要任何 JS 补丁。
both-edges 变体则是在左右两侧同时预留——适合有双向滚动需求,或者页面有「内容区和侧边栏需要严格对齐」的场景。大多数情况下 stable 就够了。
/* 只需要右边稳定 */
html {
scrollbar-gutter: stable;
}
/* 需要内容区与相邻元素严格对齐宽度 */
html {
scrollbar-gutter: stable both-edges;
}
浏览器支持:2024 年可以大胆用
scrollbar-gutter 的支持情况:
| 浏览器 | 版本 |
|---|---|
| Chrome | 94+(2021年10月) |
| Firefox | 97+(2022年1月) |
| Safari | 16.4+(2023年3月) |
| Edge | 94+(同步 Chromium) |
它已经是 Baseline 2024 的特性。如果你的项目不需要兼容 IE11,不需要兼容 Safari 15,基本可以直接用。
如果确实有旧 Safari 的兼容需求,渐进增强写法:
/* 先做兜底 */
html {
overflow-y: scroll; /* 始终显示滚动条,抖动来源于 overflow 变化,不是没有空间 */
}
/* 能力检测,渐进增强 */
@supports (scrollbar-gutter: stable) {
html {
overflow-y: auto;
scrollbar-gutter: stable;
}
}
有个坑你得知道
scrollbar-gutter: stable 解决的问题,是「内容区宽度因为滚动条消失而收缩」。但它有一个边界情况:
如果你的弹窗用的是全屏固定遮罩(position: fixed; inset: 0),inset: 0 只铺到视口内容区边缘,而 gutter 空间位于内容区之外。所以固定遮罩的右边缘会正好停在 gutter 左边那条线上,露出右侧一个细条。
这个问题在「固定遮罩 + 右侧预留 gutter」的组合下才会出现。解法是:要么接受那几像素的 margin,要么在遮罩上额外加 padding-right,要么把遮罩改成宽度 100% 的同时在 html 上用上面的 margin-left: calc(100vw - 100%) 方案。
这不是 scrollbar-gutter 的 bug,是固定遮罩定位坐标和 gutter 空间的边界问题。理解了原理,就知道什么时候该绕路。
三步下一步
-
今晚改一行:把
html { scrollbar-gutter: stable; }写进全局样式,然后去项目里搜paddingRight、scrollbarWidth、lockScroll,看看有多少处 JS 补丁可以删掉——很可能比你想象的要多。 -
清理历史包袱:项目中如果已经有
calc(100vw - 100%)方案,检查是不是和scrollbar-gutter重复了——后者更可靠,不需要给每个 fixed 元素单独补偿。 -
加渐进增强:跑一遍 Safari 16 以下的老设备测试,确保
@supports兜底逻辑正常,然后把这个属性加进你的 CSS reset / 全局 base 里,以后再写弹窗再也不碰滚动条抖动的问题。
这个问题存在了多少年?至少从 CSS Scrollbars Module Level 1 规范立项那天起,开发者就一直在用 JS 补丁模拟这个行为。Scrollbar-gutter 不是新东西,它一直在规范里,只是浏览器支持终于全了,我们终于可以把它用起来了。补丁可以退休了。
评论区
登录后可评论。