calc(100vw – 17px) 和 scrollbar-gutter: stable 差了整整一条水平滚动条——Chrome 145 把 vw 的算法扳过来了
width: 100vw 多出一条水平滚动条这件事,CSS 圈忍了十几年。以前大家的共识是「这是规范设计,只能自己减,减多少还得看系统」——Windows 上是 17px,macOS 覆盖式滚动条是 0,所以你连一个写死的 magic number 都不敢全局下手。
Chrome 145 之后,vw 终于开始感知滚动条了。但这不是无条件的:它只在滚动条「必然存在」或者「空间已经被预留」时才生效。换句话说,你得在根元素上主动开一个开关,浏览器的这份账才算得对。
先说清楚问题本身:为什么以前不能直接减
vw 这个单位从诞生起就只跟「视口宽度」有关,跟滚动条没关系。100vw 永远等于「含滚动条在内的那块宽度」。于是只要页面出现了纵向滚动条,一个 width: 100vw 的全宽容器就会比可视内容区宽出大概 15–17px,横向滚动条就这么被挤出来了。
问题在于浏览器不能无脑地永远给 vw 减掉滚动条宽度。因为一旦减了,容器变窄,页面可能就不再需要纵向滚动条;纵向滚动条一消失,内容区又变宽,横向判断再次翻转——这就是所谓的「循环渲染」(cyclic)问题。两个滚动条会互相触发、互相撤销,布局永远稳不下来。
所以过去的「正确做法」全是绕过方案:
/* 过去最常见的两个 hack */
.full-bleed {
/* 方案一:硬编码滚动条宽度 —— macOS 上就是错的 */
width: calc(100vw - 17px);
}
html {
/* 方案二:干脆把溢出藏掉 —— 会引入新的层叠上下文,
fixed/sticky 定位可能跟着出问题 */
overflow-x: hidden;
}
再讲究一点的团队会跑一段 JS,实时量出滚动条宽度写进 CSS 变量:
// 曾经的「标准解法」:靠 JS 量,靠 resize 重算
const syncScrollbarWidth = () => {
const sbw = window.innerWidth - document.documentElement.clientWidth;
document.documentElement.style.setProperty('--sbw', `${sbw}px`);
};
window.addEventListener('resize', syncScrollbarWidth);
syncScrollbarWidth();
.full-bleed { width: calc(100vw - var(--sbw, 0px)); }
这套东西能用,但要监听 resize、要防抖、首屏还得等一次 JS 执行,FOUC 一来全宽 banner 就先抖一下。
Chrome 145 做了什么:让「可预测的滚动条」变成 vw 的前提
Chrome 145 起,当根元素处于以下两种状态之一时,vw / vh 会自动把经典滚动条的厚度算进去:
overflow-y: scroll(强制滚动条常驻)scrollbar-gutter: stable(预留滚动条占位,但不强行显示)
这两个状态的共同点是——滚动条的存在是确定的、不随内容变的,那个「减了又加、加了又减」的循环被彻底切断了。这时浏览器就敢放心地给 vw 减掉滚动条宽度了。而且不只 vw,vh(横向滚动条场景)、以及 svw / lvw / dvw 这些变体都一并覆盖。
推荐用 scrollbar-gutter: stable。它只是「预留空间」,页面没有滚动条时也不会凭空多出一条滚槽,比 overflow-y: scroll 更干净。
/* 放进全局 reset 就够了,之后 100vw 才真正等于可视内容区 */
html {
scrollbar-gutter: stable;
}
/* 现在这行的语义终于对了:100vw = 视口减去滚动条 */
.full-bleed {
width: 100vw;
}
这个改动的前提是「几乎不破坏现有站点」
CSS 工作组敢拍这个板,是因为有人拿真实数据算过账:基于 HTTP Archive 的统计,受影响的页面比例大约是 0.0003%。这个量级足以说明历史行为里真正的「全宽 + 经典滚动条」组合极少,绝大多数站点的 overflow-x: hidden 早就把症状盖掉了,所以现在改默认行为不会引发大面积回归。
before / after 对照
- before:
width: 100vw全宽容器在带纵向滚动条的页面上比内容区宽约 15–17px,必须靠calc(100vw - 17px)、overflow-x: hidden或一段 resize 监听 JS 来补 - after:根元素写上
scrollbar-gutter: stable后,100vw直接等于可视内容区宽度,全宽布局不用再减一个 magic number,也省掉了首屏那次 JS 重算与布局抖动
什么时候别急着上
- 需要跨浏览器一致的生产站点:目前这是 Chrome 145+ 的行为,Firefox / Safari 的 vw 仍按旧语义算。如果你把
100vw直接当成「已减滚动条」,在别的引擎上就会窄一截。 - 更稳妥的全宽方案依然是
width: 100%(配合正常的盒模型与父容器),它对所有引擎语义一致,不依赖这个开关。
现在就能落地的一步
翻开你项目的全局 CSS reset,确认有没有 html { scrollbar-gutter: stable; } 这一行;没有就加上,然后把你代码里那些 calc(100vw - 17px) 或 calc(100vw - var(--sbw)) 的补丁逐条 grep 出来,评估能不能在 Chrome 145+ 的目标用户群里直接退回 100vw。同时保留 width: 100% 作为跨引擎的兜底写法——这一步既消掉了 magic number,也给还没跟上行为的引擎留了退路。
评论区
登录后可评论。