表头要吸顶、首列要吸左,你写了多少行滚动同步 JS——单轴滚动容器让 sticky 在两个轴各认各的祖先

表格横向能滚了,表头却永远吸不住——你为这个坑写过多少次滚动同步 JS,然后在一个新浏览器上又被偷偷改回来。

问题根源不在 sticky,在 overflow-x: auto 自己。当你给一个包裹层设 overflow-x: auto 时,CSS 会顺手把 overflow-y 也算成 auto(只要它原来不是 visible)。于是这个原本只想横向滚的容器,悄无声息地变成了纵向的滚动祖先——表头 top: 0 一吸,吸的就是这个横滚容器,而不是文档视口,纵向滚动时它一动不动。

这件事今天有了官方解法:单轴滚动容器(single-axis scroll containers)。Chrome 153 起,overflow 支持「可滚动值 + clip」混用,比如 overflow: auto clip,让容器只在真正需要的那一轴当滚动容器,另一轴只裁剪、不成为滚动祖先。

先说结论

  • 横滚 + 表头吸顶这个老问题,正解是 overflow: auto clip,不是 JS 同步
  • overflow-x: auto 会连带产生纵向滚动容器,这是 bug 的源头
  • 特性检测用 @supports named-feature(single-axis-scroll-container),别用 @supports (overflow: scroll clip)
  • 三个副作用要提前知道:程序滚动被拒、grid/flex 最小尺寸回归、滚动祖先逐轴解析

这个坑到底怎么来的

一个再普通不过的横向表格:

.table-wrapper {
  overflow-x: auto; /* 只想让表格横向滚 */
}
th {
  position: sticky;
  top: 0; /* 想让表头吸在视口顶部 */
}

结果表头不吸。因为按 CSS 规范,当 overflow-x 变成 auto 时,overflow-y 只要原本是 visible,就会被计算成 auto。.table-wrapper 因此成了纵向滚动容器,而 position: sticky 的参照系是「最近的滚动容器」——表头吸的是这个横滚框,不是视口。你滚动页面,它当然纹丝不动。

过去大家怎么破?写 JS 监听 scroll、动态算 transform、或者塞一个会滚的假表头。全是打补丁。

正解:把那一轴设为 clip

Bramus(Chrome DevRel)在 demo 里给出的修法是一行:

.table-wrapper {
  /* overflow-x: auto;  这一行让 overflow-y 也成了 auto,表头被框住 */
  overflow: auto clip;   /* 横向 create 滚动容器,纵向只裁剪、不当滚动祖先 */
}

clip 和 hidden 的关键区别在于:clip 不会创建一个滚动容器。所以 .table-wrapper 只在内联轴上是滚动容器,块轴上它什么都不是,表头的 top: 0 于是正确地指向文档视口。

顺带一个实际收益:横向滚一下,scrollIntoView() 不会再把那个被 clip 的轴也跟着动。首列的 left: 0 和表头的 top: 0 现在各自认各自的祖先——首列跟着横滚容器吸,表头跟着视口吸,不用再加一行滚动同步代码。

三个必须知道的副作用

功能是「根本性地改变滚动内容怎么被解析」,所以它带来的不只是修好一个 bug,还有几处行为变化。

第一,被 clip 的那一轴,元素会拒绝程序化滚动。也就是说,被定义为 clip 的轴,scrollTo/scrollIntoView 不会再把它当成可滚目标。这通常是好事(正是我们想要的),但如果你的代码依赖「滚动这个轴的祖先」,要重新检查。

第二,grid 和 flex 的最小尺寸会回归。网格/弹性布局里的滚动容器,默认会忽略 min-width: auto,好让单元格填满可用空间。当你在同一轴设了 overflow: clip,默认最小尺寸又开始生效了,可能把 flex/grid 容器撑大。迁移时留意布局有没有因此变形。

第三,滚动祖先逐轴解析。这既是修 bug 的机制,也是行为变更本身。任何依赖「这个元素唯一的滚动容器是谁」的逻辑都要按轴重新想一遍。

怎么做特性检测

官方建议不要用 @supports (overflow: scroll clip)——那个检查只会「能解析通过」,检测不出浏览器是否真的实现了单轴的滚动行为。

@supports named-feature(single-axis-scroll-container) {
  .table-wrapper {
    overflow: auto clip;
  }
}

JS 里同理:

CSS.supports("named-feature(single-axis-scroll-container)");

在还不支持的环境里,老写法照旧:容器还是两轴的滚动容器,表头会掉回你原来忍受的那个行为。一定要用特性查询把新旧行为分开,否则在不支持的浏览器上,overflow: auto clip 里的 clip 会被整个属性判为非法而丢弃——连 overflow-x 的横滚都没了。

该不该现在上

Chrome 官方在 190 个站点上做过兼容性测试,结论是不太会造成重大破坏。但因为它改变的是滚动内容的解析方式,影响面比一般新属性大,所以建议先在非稳定版 Chrome 上过一遍你的站。

值得现在就改的场景,恰恰是最常见的那几个:

  • 横向表格 + 吸顶表头(本文这个)
  • 横向轮播:视觉上只在一轴裁剪,但希望 scrollIntoView() 不去动另一轴
  • 任何「只想横滚、却被纵向 sticky 反噬」的包裹层

如果你手里正好有上面这类布局,下一步很具体:把定点的 overflow-x: auto 改成 overflow: auto clip,外面套一层 @supports named-feature(single-axis-scroll-container),然后在 Chrome Beta 上滚一遍,看表头和首列有没有各回各家。这一步做完,你大概能删掉一大段滚动同步 JS。

评论区

0 条评论

登录后可评论。

阿速·性能优化 1345 阅读