Skip to content

z-index 与层叠上下文

基于 CSS 现代标准 · 核于 2026-06

速查

  • z-index 只对定位元素positionstatic)及 Flex/Grid 子项生效;默认 auto;可正可负
  • z-index:层叠由文档源序决定——后写的盖在先写的之上(web.dev 原文:文档中靠后的元素压在靠前的元素上)
  • 层叠上下文(stacking context):一组「一起沿 Z 轴升降」的元素;<html> 本身就是一个,且没有任何东西能排到它后面
  • 关键定律z-index 只在同一个层叠上下文内可比;子上下文被当作「一个整体」参与父上下文的层叠——这是「z-index: 9999 仍被压住」的根因(z-index 困局)
  • 创建层叠上下文的常见条件:根元素;定位 + z-index≠autofixed/sticky;Flex/Grid 子项 + z-index≠autoopacity<1transform/filter/backdrop-filter/perspective/clip-path/mask(非 none);mix-blend-mode≠normalisolation:isolatewill-change 指定上述属性;contain:layout/paint/strict/contentcontainer-typenormal;顶层元素及其 ::backdrop
  • 上下文内七层层叠顺序(自底向上):①创建上下文元素的背景/边框 → ②负 z-index 子上下文 → ③块级盒 → ④浮动盒 → ⑤行内/行内块盒 → ⑥z-index:auto/0 的定位元素 → ⑦正 z-index 子上下文
  • 破局工具:isolation: isolate 主动开一个干净的层叠上下文,把内部 z-index 战争关进笼子
  • 顶层逃逸:popover / <dialog> 模态进顶层,完全无视 z-index 与祖先 overflow

z-index 的前提:先定位

z-index 控制元素在 Z 轴(指向用户的方向)上的层级,数值大的在上、小的在下,可以是负数。但它有前提——只对定位元素生效

css
.up-front {
  position: relative; /* 没有这一行,z-index 在普通流里无效 */
  z-index: 2;
}

唯一例外:Flex 容器或 Grid 容器的直接子项,即使不写 positionz-index 也生效(web.dev 明确指出 flexbox/grid 语境下不需要 position)。除此之外,给 static 元素写 z-index 一律无效。

没有 z-index 时:源序决定一切

很多人以为「不写 z-index 就没有层叠顺序」,其实浏览器有一套默认规则。web.dev 的总结最直白:

如果你没有给元素设 z-index,默认行为就是由文档源序决定 Z 轴顺序——这意味着文档中靠后的元素会盖在靠前的元素之上。

所以两个重叠的定位元素,谁都不写 z-index 时,HTML 里写在后面的那个在上面。完整的「同一上下文内」层叠顺序见后文七层模型。

层叠上下文:一起升降的一组元素

理解 z-index 的钥匙是「层叠上下文」。web.dev 的定义:

层叠上下文是一组拥有共同父级、并作为一个整体沿 Z 轴一起升降的元素

关键推论:子元素的 z-index 永远是相对于「它所在那个层叠上下文里、父级的当前次序」而言的<html> 元素自身就是最顶层的层叠上下文,没有任何东西能排到它后面。

一旦某个元素「创建了层叠上下文」,它内部所有后代的层叠就被封进这个上下文:后代之间可以用 z-index 互相比较,但它们无论写多大的 z-index,都无法越过这个上下文整体在父级里的排位。这就是下面那个困局的根源。

z-index 困局:9999 为什么还压不住

这是面试最爱、实战最常踩的一题。看这段结构:

html
<div class="ctx-a">
  <div class="overlay">我 z-index: 9999</div>
</div>
<div class="ctx-b">普通内容</div>
css
.ctx-a {
  position: relative;
  z-index: 1;       /* ← ctx-a 创建了一个层叠上下文,排位 1 */
}
.overlay {
  position: absolute;
  z-index: 9999;    /* ← 这个 9999 只在 ctx-a 内部有意义 */
}
.ctx-b {
  position: relative;
  z-index: 2;       /* ← ctx-b 排位 2,整体高于 ctx-a 的 1 */
}

结果:.overlayz-index: 9999 压不过 .ctx-b。因为 .overlay 被困在 .ctx-a 这个排位为 1 的上下文里,而 .ctx-b 排位 2——父上下文整体的排位先比,子元素的 z-index 只在自己的上下文里有效。MDN 把这形容为「版本号」:子元素的 z-index 是父级大版本号下的小版本号,1.9999 永远小于 2.0

破解困局的正确姿势

不要无脑加大 z-index——那是在错误的上下文里加。正确做法是调整「创建上下文的那一层」的 z-index(把 .ctx-az-index 提到比 .ctx-b 高),或者干脆不让中间层创建多余的上下文(去掉那个随手写的 z-index: 1transform / opacity<1)。很多「z-index 失控」其实是某个祖先无意中创建了层叠上下文。

什么会创建层叠上下文

记不住没关系,但要知道「创建层叠上下文的条件比想象中多得多」——很多看似与层级无关的属性都会悄悄开一个新上下文。常见触发条件(据 MDN):

  • 根元素 <html>
  • 定位 + z-index≠autoposition: relative/absolutez-indexauto
  • fixedsticky:只要定位就创建(与 z-index 无关);
  • Flex / Grid 子项 + z-index≠auto
  • opacity 小于 1
  • mix-blend-modenormal
  • 以下属性取非 nonetransformscalerotatetranslatefilterbackdrop-filterperspectiveclip-pathmask / mask-image / mask-border
  • isolation: isolate
  • will-change 指定了任何「会创建层叠上下文」的属性;
  • contain: layout / paint / strict / content
  • container-typesizeinline-size(容器查询);
  • 顶层元素及其 ::backdrop(全屏、popover、模态 <dialog>);
  • @keyframes 动画了 opacity 等属性且 animation-fill-mode: forwards

最坑的三个「隐形」触发器

opacity: 0.99transform: translateZ(0)(常被用作「GPU 加速 hack」)、will-change: transform——它们都不改变外观或位置,却都会创建层叠上下文,让内部 z-index 与外界隔绝。当 z-index 行为诡异时,沿 DOM 往上 grep 这几个属性,往往就是元凶。

上下文内部的七层层叠顺序

同一个层叠上下文内部,元素从底到顶按这个固定顺序绘制(MDN 的七层模型):

内容
1(最底)创建该上下文的元素的背景与边框
2z-index 的子层叠上下文(z-index < 0
3正常流中的块级盒(非定位)
4浮动盒(非定位)
5正常流中的行内 / 行内块盒(非定位)
6z-index: autoz-index: 0定位元素
7(最顶)z-index 的子层叠上下文(z-index > 0

几个由此得出的常识:

  • z-index 能钻到背景之上、但在普通块级内容之下(第 2 层在第 3 层下方)——这就是「把装饰元素用 z-index: -1 垫到文字背后」的原理;
  • 定位元素(哪怕 z-index:0)默认压在普通块级、浮动、行内内容之上(第 6 层 > 第 3~5 层)——所以一个 position: absolute 的元素天然会盖住旁边没定位的文字;
  • 同层内部再按源序决定先后。
css
.hero {
  position: relative;
}
.hero__bg {
  position: absolute;
  inset: 0;
  z-index: -1; /* 钻到 .hero 文字背后,但仍在 .hero 背景之上 */
}

主动隔离:isolation: isolate

当你写一个可复用组件,又不想让它内部的 z-index 跟外部「打架」,可以用 isolation: isolate 主动创建一个干净的层叠上下文——它不改变任何视觉效果,只是把内部的层叠战争关进笼子:

css
.widget {
  isolation: isolate; /* 内部 z-index 从此与页面其余部分隔离 */
}

这是唯一一个「专门用来创建层叠上下文、且零副作用」的属性,比用 transformopacity 去「凑」一个上下文要干净得多。

终极逃逸:顶层(top layer)

有一类元素根本不参与 z-index 游戏——进入**顶层(top layer)**的元素:用 popover 属性的弹层、用 showModal() 打开的模态 <dialog>、全屏元素。它们渲染在所有普通内容之上,完全无视祖先的 z-indexoverflow: hidden 裁剪,从根本上绕开了层叠上下文的所有困局。这正是现代「弹层不再需要 z-index: 99999」的底气,详见 popover & dialog 与定位

小结

z-index 只对定位元素生效、只在同一层叠上下文内可比;满屏 z-index: 9999 治不了「被困在低排位父上下文」的病,调对「创建上下文那一层」或用 isolation: isolate 隔离才是正解。讲完层级,回头补一块定位之前的遗留地基——float / clear 与清除浮动