Cairn
← 返回博客

导航栏为什么会横跳:滚动条与视口宽度

导航栏为什么会横跳:滚动条与视口宽度

有个前端问题很会装无辜:

首页的导航栏是居中的,文章列表页的导航栏也是居中的,两个页面甚至共用同一份模板。可从首页点进文章列表时,导航栏还是轻轻横移了一下。

只移动几个像素,不影响使用,却足以让人怀疑自己的 CSS。

这个问题通常和导航栏无关。真正动的是浏览器的可用视口宽度。

先确认它真的移动了

不要一上来就改 paddingmargintransform。先在两个页面分别打开控制台,记录下面几个值:

const nav = document.querySelector("nav");

console.table({
  innerWidth: window.innerWidth,
  clientWidth: document.documentElement.clientWidth,
  navLeft: nav.getBoundingClientRect().left,
  navWidth: nav.getBoundingClientRect().width,
});

其中:

  • window.innerWidth 通常包含垂直滚动条占用的宽度;
  • document.documentElement.clientWidth 通常不包含滚动条;
  • 两者之差,就是经典滚动条可能占掉的空间。

如果短页面的 innerWidthclientWidth 相同,而长页面相差十几个像素,原因基本已经找到了。

为什么刚好只偏几像素

假设浏览器滚动条宽度是 16 像素。

没有滚动条时,内容区域宽 1440 像素;出现滚动条后,只剩 1424 像素。一个通过 margin-inline: auto 居中的容器,会相对左右边界重新计算位置,于是左侧大约移动:

16 / 2 = 8 像素

这就是为什么页面不是突然错位一大截,而是“不太明显,但怎么看都不顺眼”。

首页内容较少,没有垂直滚动条;文章列表较长,出现了滚动条。两个页面的 CSS 没变,计算 CSS 的画布却变窄了。

首选修复:提前预留滚动条空间

现代浏览器可以使用:

html {
  scrollbar-gutter: stable;
}

stable 的意思是:即使当前页面不需要滚动条,也给它保留位置。这样页面在短内容和长内容之间切换时,可用宽度不会来回变化。

如果页面两侧都需要对称留白,还可以使用:

html {
  scrollbar-gutter: stable both-edges;
}

普通网站一般用 stable 就够了。both-edges 会额外占用另一侧空间,不要因为名字听起来更完整就顺手加上。

兼容方案:始终显示垂直滚动条

如果需要兼容不支持 scrollbar-gutter 的旧浏览器,可以写:

html {
  overflow-y: scroll;
}

这样短页面也会保留滚动条轨道。优点是简单稳定,缺点是即使内容不足一屏,浏览器仍会显示一条不能滚动的轨道。

可以组合使用:

html {
  overflow-y: scroll;
}

@supports (scrollbar-gutter: stable) {
  html {
    overflow-y: auto;
    scrollbar-gutter: stable;
  }
}

支持新属性的浏览器使用 scrollbar-gutter,旧浏览器退回到始终保留滚动条。

为什么有时在自己电脑上复现不了

macOS 等系统可能使用覆盖式滚动条:滚动条浮在页面上方,不挤占布局宽度。此时 innerWidthclientWidth 可能完全相同。

到了 Windows、Linux,或者用户把系统设置改成“始终显示滚动条”,问题才会出现。

因此不能只凭“我这里没动”判断问题不存在。至少要检查:

  1. 一个内容不足一屏的页面;
  2. 一个内容明显超过一屏的页面;
  3. 使用传统滚动条的浏览器环境;
  4. 打开和关闭模态框时的页面变化。

模态框是同一个问题的另一个入口

很多模态框打开时会给 body 设置:

body {
  overflow: hidden;
}

这会让原本存在的滚动条突然消失,页面反而向另一边移动。

成熟的模态框组件通常会计算滚动条宽度,并临时给页面增加等宽的右侧内边距。自己实现时,可以这样测量:

const scrollbarWidth =
  window.innerWidth - document.documentElement.clientWidth;

打开模态框:

document.body.style.overflow = "hidden";
document.body.style.paddingRight = `${scrollbarWidth}px`;

关闭时清理:

document.body.style.overflow = "";
document.body.style.paddingRight = "";

如果页面已经使用 scrollbar-gutter: stable,仍然要在目标浏览器里验证模态框行为,因为不同浏览器对根元素和 body 滚动的处理并不完全一致。

别用肉眼结束调试

修复后,再运行开头的测量代码。重点不是导航栏的 left 必须一模一样,而是页面切换前后的内容视口是否稳定。

还可以给导航栏临时加一条参考线:

nav::after {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 50vw;
  width: 1px;
  background: red;
  content: "";
  pointer-events: none;
}

如果导航内容始终围绕参考线居中,修复就生效了。验证结束后删掉这段调试样式。

一条更通用的调试经验

元素看起来移动时,不一定是元素自己的样式变了。也可能是:

  • 父容器宽度变了;
  • 字体加载后文字尺寸变了;
  • 图片没有预留尺寸,加载后撑开布局;
  • 页面滚动条出现或消失;
  • JavaScript 给 body 增删了 class;
  • 浏览器缩放或系统滚动条策略不同。

先测量,再归因。否则很容易为了补偿 8 像素加上一条 margin-left: 8px,然后在另一台电脑上制造一个全新的 8 像素问题。

前端最让人哭笑不得的地方就在这里:一个肉眼只看得见几像素的抖动,背后可能站着一整套浏览器布局规则。

评论

暂无评论,来说点什么吧。

请先登录后再发表评论

去登录