原因永远在祖先身上
一句话总结
当 fixed 没有固定在屏幕上、sticky 没有跟随滚动时,原因不在这个元素本身,而在祖先。只要找出是谁创建了包含块和滚动容器,答案就出来了。
为什么需要它
这两个缺陷的症状完全没有指向原因。
- 给模态框遮罩设置了
position: fixed; inset: 0,却没有盖满整个屏幕,只铺在卡片里面 - 给目录设置了
position: sticky; top: 64px,一滚动它就直接上去了
两者中这个元素的 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 的层叠上下文文档 给出了完整的列表。
position不是 static,并且z-index不是auto的元素position: fixed和position: sticky(与 z-index 无关,始终如此)opacity小于 1transform、filter、perspective、isolation: isolate、contain: paint
如果祖先创建了层叠上下文,其中的 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 分钟内解决。
- 这个元素的
position是什么- 包含块是什么——沿着祖先向上查找
transform一类的属性- 如果是 sticky,有没有阈值,滚动容器是哪一个
- 如果是 z-index,祖先中是谁创建了层叠上下文
下一项实验要做什么
在给定的文档结构上编写样式表,并用计算器亲自找出包含块和滚动容器。 亲眼确认给卡片加上 transform 之后遮罩是怎样被困住的,以及祖先的 overflow: hidden 是怎样让 sticky 失效的。