文章

CSS定位与层叠上下文深入解析

position 五种取值的区别、z-index 为何失效、层叠上下文与包含块原理,前端布局基本功面试必考。

CSS定位与层叠上下文深入解析

一句话概括

CSS 定位解决两件事:元素”在哪儿”(位置参考系) 和 “谁压在谁上面”(堆叠顺序)。前者靠 position,后者靠 z-index,但两者都绕不开一个核心概念——层叠上下文(Stacking Context)。

一句话记住:定位是前端最容易出 bug、也最见基本功的地方。你遇到过的”z-index 设了 99999 还是被挡”、”fixed 子元素不相对视口”、”sticky 死活不吸顶”,根源全在定位规则和层叠上下文上。

核心知识点

1. position 的五种取值

1
2
3
4
5
.box { position: static; }    /* 默认,正常文档流,top/left/z-index 全无效 */
.box { position: relative; }  /* 相对自身原位置偏移,原空间保留 */
.box { position: absolute; }  /* 脱离文档流,相对最近的非 static 祖先 */
.box { position: fixed; }     /* 脱离文档流,相对视口 */
.box { position: sticky; }    /* 阈值内像 relative,超过阈值像 fixed */
  • relative:视觉偏移,但原来的位置仍被占着,不影响别人
  • absolute:完全脱档,不占空间,找”最近的定位祖先”当参照
  • fixed:脱档,参照视口(有个大坑见第 6 节)
  • sticky:relative 和 fixed 的混合体

2. 绝对定位的参照系:最近的”非 static 定位祖先”

1
2
3
<div class="parent">          <!-- 设了 position: relative -->
  <div class="child"></div>   <!-- absolute,相对 .parent 定位 -->
</div>
1
2
.parent { position: relative; width: 300px; height: 300px; }
.child  { position: absolute; top: 0; right: 0; }  /* 贴在 .parent 右上角 */

如果所有祖先都是 static,absolute 元素就找”初始包含块”(基本等于视口)当参照。常见坑:父元素只写了 transform 却没写 position,absolute 子元素并不会以它为参照——必须父元素是 relative/absolute/fixed 之一。

3. sticky 的三要素与失效排查

1
.sticky-nav { position: sticky; top: 0; }  /* 滚到距顶 0px 时吸住 */

sticky 要生效,三个条件缺一不可:

  1. 设了 position: sticky
  2. 设了阈值(top/bottom/left/right 至少一个)
  3. 父容器能滚动、且没把溢出裁掉
1
2
/* ❌ 最常见失效原因:祖先 overflow: hidden 把滚动裁掉了 */
.ancestor { overflow: hidden; }

失效排查顺位:① 祖先 overflow: hidden/auto 裁剪了 → ② 父容器高度不够、没有滚动空间 → ③ 忘了设阈值 top/bottom。

4. 层叠上下文:z-index 只在”同一上下文”里比

层叠上下文像一个三维的”楼层”。元素创建了自己的层叠上下文后,它的所有子元素的 z-index 只能在这个楼层内部比,不能和外面跨层比较。

所以”z-index: 99999 还被挡”的真正原因:你的元素被关在某个父层叠上下文里,对手在另一个更高的楼层,你再大也只是在自家楼层里称王。

5. 什么会创建层叠上下文

触发条件例子
根元素<html> 本身就是一个层叠上下文
定位 + z-index 非 autoposition: relative; z-index: 1
position: fixed / sticky直接创建(即使 z-index 为 auto)
opacity < 1opacity: 0.9
transform / filter / perspective 非 nonetransform: scale(1)
will-change 指向上述属性will-change: transform
isolation: isolate强制隔离创建
mix-blend-mode 非 normal混合模式

记忆:“定位带 z-index、transform、opacity、filter”是高频创建源,调试 z-index 时先把这些列出来。

6. 大坑:transform 改变 fixed/sticky 的包含块

1
2
3
.parent { transform: translate(0); }   /* 即使只是 translate(0) */
.child  { position: fixed; top: 0; }
/* ❌ .child 不再相对视口,而是相对 .parent! */

transform(以及 filter、perspective)会为后代元素创建新的包含块(containing block)。fixed 元素原本参照视口,一旦祖先有 transform,就改参照那个祖先了。这也是很多人”加了 transform 后弹窗定位全乱”的根源。

z-index 还有个例外:flex/grid 的直接子项,即使 position: static,z-index 也有效(用来调子项堆叠)。

其实你每天都在用

  • 弹窗遮罩盖住全页:position: fixed + 高 z-index 蒙层
  • 吸顶导航 / 表头:position: sticky; top: 0
  • 卡片右上角的关闭按钮:absolute 相对卡片定位
  • 下拉菜单浮在内容上方:absolute + z-index 浮层
  • modal 层级高于 header:用 CSS 变量统一分层(--z-modal > --z-header)
  • 图片角标 / 优惠券标:absolute 贴边
  • 滚动时目录高亮跟随:sticky 侧边栏,滚到哪亮到哪

常见误解(FAQ)

❌ 误区一:”z-index 设成 999999 就一定在最上面”

不一定。z-index 只在同一个层叠上下文里比大小。如果你的父元素自己在一个低楼层(比如父元素 opacity:0.9 或 transform 创建了上下文、且楼层号低),你再大的 z-index 也只在父楼层内部比,赢不了楼上的对手。排查要顺着祖先链找”谁创建了上下文、楼层号多少”。

❌ 误区二:”position: fixed 一定相对视口”

有个大坑:一旦 fixed 元素的祖先有 transform/filter/perspective,fixed 就改成相对那个祖先定位了(因为祖先成了新的包含块)。所以”加了 transform 后 fixed 跑偏”不是 bug,是规范行为。

❌ 误区三:”sticky 不生效是浏览器 bug”

绝大多数情况是自己写法问题:① 祖先 overflow: hidden 把滚动裁了(最常见);② 父容器没有足够高度、滚不起来;③ 忘了设 top/bottom 阈值。先查这三条。

❌ 误区四:”z-index 对所有元素都有效”

只对定位元素(relative/absolute/fixed/sticky)有效;普通 static 元素写 z-index 是无效的(flex/grid 直接子项是例外,可生效)。忘了加 position 是常见的”设了没反应”。

❌ 误区五:”层叠上下文只能靠 z-index 创建”

错。除了 position+z-index,transform、opacity<1、filter、will-change、isolation: isolate、position: fixed/sticky 等都会创建层叠上下文。很多”莫名其妙被挡”就是这些隐性上下文在作怪。

一句话总结

定位管”在哪儿”、z-index 管”谁在上”,但真正决定胜负的永远是层叠上下文——记住”z-index 只在同一楼层比大小、transform 会让 fixed 改认祖宗”,你就能从”拍 99999 玄学”变成”按规则排楼层”的靠谱前端。

本文由作者按照 CC BY 4.0 进行授权