文章目录

七个可以当场验证的 CSS / 浏览器行为

技术文章里最没用的句式是「据说」和「一般来说」。

这篇文章的做法是:每条断言都配一个能在你的浏览器里当场跑的实验。你不需要相信我 —— 打开页面,实验台会自己测、自己判、把数值打出来。

实验台地址:https://www.hpleapfrog.cn/lab/css-facts/

下文所有数字,都是我在 Chrome 内核(Edge)1600×900 下跑出来的原始输出。换浏览器、换屏幕宽度,结果可能不同 —— 那正是你应该自己去点一下的原因。


为什么值得”能验真伪”

我在别处写过,「我验证过了」这句话本身没有信息量,有信息量的是「验证了什么范围」。

所以这次我把范围也一起交出去了:不是”我测过”,而是”这个页面会为你再测一遍”。

实验台的结构很简单:

  • 每个实验在页面里新建真实 DOM 夹具
  • 用 getComputedStyle / getBoundingClientRect / scrollTop 读真实数值
  • 打印观测值 + 判定,并说明”什么样的数值才算成立”
  • 任何一步失败,它自己会显示”夹具无效”,而不是硬给个结论

实验 1:overflow-x: hidden 会连带改变 overflow-y

断言:给一个盒子设 overflow-x: hidden,它的 overflow-y 计算值会变成 auto。

观测:overflow-x:hidden  → overflow-y: auto
      (对照)overflow-x:visible → overflow-y: visible
判定:✓ 成立

为什么值得知道:这条我自己踩过。为了”防横向溢出”顺手写一行 overflow-x: hidden,等于同时创建了一个纵向滚动容器 —— 而”祖先有滚动容器”正是 position: sticky 失效的头号原因(见实验 6)。

一行防溢出的 CSS,静默废掉整页的 sticky。


实验 2:overflow: clip 不创建滚动容器

断言:overflow: clip 裁剪内容,但不创建滚动容器。

观测:overflow:clip   容器 scrollTop 赋值后 = 0
      overflow:hidden 容器 scrollTop 赋值后 = 100
判定:✓ 成立

为什么有用:clip 和 hidden 视觉上都是”裁掉溢出”,但语义不同。如果你的目的只是裁剪(而不是”可滚动”),用 clip 就不会顺手破坏 sticky。

浏览器支持:Chrome 90+ / Firefox 81+ / Safari 16+。


实验 3:getBoundingClientRect() 返回的是变换后的尺寸

断言:getBoundingClientRect() 给出的是经过 CSS transform 之后的视觉盒模型。

观测:scaleX(2) 前 width = 100,后 = 200
判定:✓ 成立

为什么是个坑:当你用 rect 去算布局、做对齐、或者判断”是否溢出”时,父级任何 scale 都会污染这个数值。

想要未变换的布局尺寸,应该用 offsetWidth / offsetHeight(它们不受 transform 影响)。


实验 4:innerWidth − clientWidth = 滚动条宽度

断言:两者的差值就是经典滚动条占掉的宽度。

观测:innerWidth = 1570,clientWidth = 1555,差 = 15
判定:✓ 成立,本机滚动条 15px

为什么重要:如果你用 window.innerWidth 判断”元素是否居中”,会引入这 15px 的偏差。

我因为这条真的差点去修一个不存在的问题 —— 当时算出”居中偏差 11px”,一度以为 CSS 有问题。实际是测量口径错了:判断居中要用 document.documentElement.clientWidth。

同一个页面,两个都对的答案

这条实验有个有意思的地方:我用无头浏览器(headless)跑它时,测出来是 差 = 0。

同一份代码、同一台机器、同一个浏览器内核 —— 结论却是「15px」和「0」。

原因不是谁错了,而是环境的差异:

环境 滚动条类型 差值
有头浏览器(经典滚动条) 占据布局宽度 15px
无头浏览器 / 移动端 覆盖式(overlay) 0

而这恰好就是这篇文章想说的那件事 —— 实验台把两种情况都如实报出来,第二种会标成「差为 0(覆盖式滚动条/移动端)」,而不是硬套”成立”。因为它没法替你判断”你这个环境该不该有滚动条”。

如果你打开实验台看到的是「0」,不要以为实验失败了 —— 那说明你的环境用的是覆盖式滚动条。这本身就是一条关于你设备的信息。

⚠️ 推论:任何用 innerWidth - clientWidth > 0 来”检测是否有滚动条”的代码,在移动端会全部失效。


实验 5:特异性相同时,后出现者获胜

断言:两条规则特异性完全相同时,源码中后出现的那条生效。

#spec-probe { color: rgb(1,2,3) }
#spec-probe { color: rgb(4,5,6) }
观测:计算值 = rgb(4, 5, 6)
判定:✓ 成立

为什么值得记:这是层叠规则的最后一道判据。当你需要覆盖第三方样式、又不想无限堆高特异性时,「加载顺序」往往是更干净的手段 —— 把你的样式表放在最后。


实验 6:任一祖先有 overflow,sticky 就失效

断言:只要任一祖先设了 overflow: hidden / auto / scroll,position: sticky 就失效。

观测:实际滚动量 300 / 300
      sticky 相对滚动容器顶部偏移 ——
        祖先 overflow:hidden:  -299   ← 跟着滚走了
        祖先 overflow:visible:    1   ← 吸住了
判定:✓ 成立

怎么读这个数:偏移 ≈ 0 表示吸住;偏移 ≈ −300 表示它完全跟着内容滚走了。

⚠️ 顺带说一个我自己写错的夹具

这个实验我第一版做错了,值得单独讲,因为它是个典型的验证陷阱。

第一版我把 sticky 元素放在了 800px 撑高块的后面。滚动 300px 时,它根本还没到达容器顶部 —— 所以两个盒子都没吸住,测出来是:

祖先 overflow:hidden:  501
祖先 overflow:visible: 501     ← 对照组也没吸住!

是对照组救了我。 我当时的判定逻辑面对这组数字给出了「不成立」(因为预期”hidden 下不吸、visible 下吸”),但真正的原因是夹具错了,不是 CSS 行为特殊。

如果当时没有 overflow: visible 这个对照组,我会看到单个 501,很可能解释成”overflow 不影响 sticky” —— 那是一个与所有资料相反的结论,而且看起来很像是”我亲手测出来的”。

修正两步:

  1. 把 sticky 放到撑高块之前(它才会经过容器顶部);
  2. 在判定逻辑里先检查滚动是否真的发生 —— 没滚起来就直接标「夹具无效」,不参与断言判定。

    修正后:实际滚动量 300 / 300
        overflow:hidden:  -299   ← 跟着滚走了
        overflow:visible:    1   ← 吸住了
    

教训:对照组的作用不是让结论更好看,而是让”实验本身坏了”这件事变得可见。有对照组的实验才能区分「断言不成立」和「夹具写错了」—— 没有对照组,这两种情况长得一模一样。

顺带一提:这也是我坚持把「夹具有效性检查」写进实验台的原因。一个不会承认自己失效的实验,比没有实验更危险。


实验 7:flex 子项被 stretch → sticky 无处可粘

断言:flex 容器的子项默认被 align-items: stretch 拉满高度,导致 sticky 没有可粘的余地;加 align-self: flex-start 才生效。

观测:默认(stretch):高度 800px,滚动 300px 后偏移 -299   ← 没吸住
      align-self:flex-start:高度 26px,偏移 1             ← 吸住了
判定:✓ 成立

这是我自己踩的那个坑。 三栏布局里给左栏加 position: sticky,完全不生效 —— 因为左栏被 stretch 拉到和整列一样高,高度相同就没有滚动余地。

补上 align-self: flex-start,立刻生效。

关键点:这个错误不会报任何警告。相同的 position: sticky 写在普通块级容器里是好的,搬进 flex 布局里就哑了。


我把它们串起来

这七个实验不是孤立的,它们能和上一篇《sticky 失效决策树》对上:

实验 对应决策树里的第几种失效
1(overflow-x 联动) 第 2 种 —— 解释了为什么”防溢出”会毁掉 sticky
2(clip 不建滚动容器) 第 2 种的解法之一
6(祖先 overflow) 第 2 种,直接验证
7(flex stretch) 第 4 种,直接验证

而实验 4 和 5 解释了另一件事:为什么我在判断”居中”时会误报 11px(口径错),以及为什么”把样式表放最后”能救回被第三方覆盖的样式。


关于这份实验台

它是纯静态页面,没有后端、不采集数据。源码就在这个博客的仓库里: static/lab/css-facts/index.html,一百多行原生 JS,可以直接读。

它有几个我认为很重要的设计:

  1. 每条断言都打印”观测值”,而不只是”成立 / 不成立” —— 你可以自己核对数字。
  2. 带对照组。没有对照组的实验只能证明”发生了 X”,不能证明”是 Y 导致 X”。
  3. 能承认失效。夹具没滚起来、依赖的 API 不存在时,它标”夹具无效”,而不是硬给结论。

如果你在别的浏览器上跑出不一样的结果,那比这篇文章本身有价值。

See Also