TT Lab
开始
学习 学习路径 课程

CSS — 后写的规则获胜

原因永远在祖先身上

在 TT Lab 中继续学习

一句话总结

当 fixed 没有固定在屏幕上、sticky 没有跟随滚动时,原因不在这个元素本身,而在祖先。只要找出是谁创建了包含块和滚动容器,答案就出来了。

为什么需要它

这两个缺陷的症状完全没有指向原因。

两者中这个元素的 CSS 都完全正确。用开发者工具只看这个元素,不会发现任何问题。所以人们会一边改数值一边耗上好几个小时。该看的地方是祖先。

工作原理——包含块

top、left、width: 50% 之类的值,全部以包含块(containing block)为基准来度量。这个包含块是什么,由 position 的值决定。MDN 的包含块文档 整理了规则。

position 包含块
static · relative 最近的块级祖先的 content 区域
absolute 最近的已定位(不是 static)祖先的 padding 区域
fixed 视口
sticky 文档流的基准是父元素,粘附的基准是最近的滚动容器

到这里为止,大部分人都知道。问题在后面。

会困住 fixed 的属性

只要祖先中有任何一个带有下面的属性,这个祖先就会成为 fixed 和 absolute 的新包含块。

transform          (none 이 아닌 값)
filter
backdrop-filter
perspective
will-change: transform | filter | perspective
contain: layout | paint | strict | content

该代码块中的韩文说明的意思是:transform 取非 none 的值。

于是就会出现这样的事。为了让鼠标悬停在卡片上时有轻微浮起的效果,加上了 transform: translateY(-2px)。或者为了性能,加上了 will-change: transform。就在那一刻,原本在卡片内的模态框遮罩被困在了卡片里。动画和模态框看起来毫无关系,所以要花很长时间才能找到原因。

CSS Positioned Layout 3 和 CSS Containment 3 分别包含各自的规定。

sticky 不粘附的两个原因

position: sticky 必须同时满足两个条件才能起作用。

第一,必须有阈值。 top、right、bottom、left 中至少有一个不能是 auto。一个都没有的话,就没有可以粘附的线,它只会随着文档流走。既没有错误,也没有警告。

第二,滚动容器必须真的会滚动。 sticky 只会在最近的滚动容器内粘附。滚动容器就是 overflow 不是 visible 的最近的祖先。

这里 overflow: hidden 会造成事故。为了阻止横向滚动,在中间的容器上加了 overflow-x: hidden,overflow-y 也会随之变成 auto,于是这个盒子就成为滚动容器了。用户不会去滚动这个盒子(而是滚动页面),所以 sticky 永远不会有粘附的机会。

而且 sticky 的粘附范围在父盒子之内。如果父元素只有自己那么高,它就只会跟随那么一段。“跟了一小段就不跟了”这个症状,几乎总是这个原因。

z-index 在哪个盒子内较量

明明设置了 z-index: 9999,却还是被压在后面。不是因为数字小,而是因为处在另一个层叠上下文之内。

层叠上下文由下面这些情况创建。MDN 的层叠上下文文档 给出了完整的列表。

如果祖先创建了层叠上下文,其中的 z-index 只在这个上下文内部起作用。父元素是 z-index: 1,兄弟元素是 z-index: 2,那么即使父元素内的子元素是 9999,它也在那个兄弟元素之下。opacity: 0.99 这样不经意的一行,就会造成这种情况。

在现场相遇的样子

有三种情况反复出现。

第一,下拉菜单被父元素裁掉了。 祖先上有 overflow: hidden,而下拉菜单是 absolute。改成 fixed 就可以了,但如果这个祖先上有 transform,fixed 也会被困住。根本的解决办法是把菜单移到文档最顶层(Portal)。

第二,只有在移动端,标题栏才不粘附。 原因是有一个媒体查询,为了在窄屏幕上防止横向溢出而加了 overflow-x: hidden。

第三,只有在 iOS 上才有问题。 这通常是 -webkit-overflow-scrolling 或安全区域(env(safe-area-inset-*))的问题,与这里讲的规则不在同一条轴上,必须在真机上确认。最好不要凭猜测去修改。

只要记住诊断顺序,大多数情况都能在 5 分钟内解决。

  1. 这个元素的 position 是什么
  2. 包含块是什么——沿着祖先向上查找 transform 一类的属性
  3. 如果是 sticky,有没有阈值,滚动容器是哪一个
  4. 如果是 z-index,祖先中是谁创建了层叠上下文

下一项实验要做什么

在给定的文档结构上编写样式表,并用计算器亲自找出包含块和滚动容器。 亲眼确认给卡片加上 transform 之后遮罩是怎样被困住的,以及祖先的 overflow: hidden 是怎样让 sticky 失效的。