CSS 与布局 / CSS stacking context · 拆解 待审核
z-index · stacking order

CSS stacking context · 拆解

为什么 z-index: 9999 有时压不住别的元素?把页面看成一摞文件夹,逐节厘清 stacking context 的排序法则、触发条件、isolation: isolate 的用途,以及三个最常见的排错场景。

阅读本页默认已了解 z-indexposition 的基本含义:同一个 stacking context 内,z-index 大的元素叠在上面、相同时按文档顺序。本页要解决的是另一层问题——跨 stacking context 时,z-index 的比较为何不再成立。

取材自 Smashing Magazine《Unstacking CSS Stacking Contexts》(2026-01),并按 MDN 补足触发清单。四节依次是排序法则谁会创建-stacking-contextisolation-isolate三个常见排错场景

1 · 排序法则

把页面想成一张桌子,每个元素是一张纸,后写的盖在先写的上面。stacking context 相当于一个文件夹:一张纸一旦被放进某个文件夹,它就只在该文件夹内部参与排序,既不会跑到文件夹外,也不会插到别的文件夹的纸中间。

因此浏览器决定元素叠放顺序时,遵循一条核心法则——先按 stacking order 把文件夹整体排序,再排每个文件夹内部的纸。子元素的 z-index 只在自己所属的 stacking context 内有意义,不会拿去和别的 stacking context 里的元素直接比较。

1.1 · 两个文件夹与两张纸

图 1-1 · 两个面板各自是一个 stacking context(position:relative 加 z-index),各装一个会探出边界、互相重叠的子徽标。可把红子徽标的 z-index 拉到 9999,看它能否压过蓝子徽标。

纸 A 无论 z-index 多大,只要它所在的文件夹 A 整体排在文件夹 B 下面(stacking order 更低),纸 A 就始终在纸 B 下面。z-index: 9999 在跨 stacking context 的比较里根本不参与计算。

1.2 · 根在哪里

最外层的根 stacking context 由 <html> 元素构成。只要一个元素没有创建新的 stacking context,它的子孙就直接参与最近祖先 stacking context 的排序——这也是为什么没设 z-index 时,页面看起来扁平、按文档顺序叠放。至于哪些属性会创建一个新的 stacking context,见下一节

2 · 谁会创建 stacking context

能创建新 stacking context 的属性相当多,因此常常会无意间创建出一个、把子元素限制在内部。图 2-1 用一个跨层级测试把这些属性逐一辨认出来。

图 2-1 · 舞台里有一条 z-index:1 的红条;蓝色测试盒本身没设 z-index(auto),里面塞了个 z-index:999 的子徽标。测试盒没创建 stacking context 时,子徽标凭 999 压在红条之上;一旦创建了,子徽标被限制在盒内、整盒按 auto 排序,于是沉到红条之下。可逐个开启属性,看徽标在哪一项下沉底。

2.1 · 完整清单

下面任意一条成立,该元素就会创建一个新的 stacking context(按 MDN 口径,核对于 2026-08):

  • 根元素 <html>
  • position: absoluterelative,且 z-index 不为 auto
  • position: fixedsticky(自身即创建,无需 z-index)
  • container-typesizeinline-size
  • flex 或 grid 容器的子项,且 z-index 不为 auto
  • opacity 小于 1
  • mix-blend-mode 不为 normal
  • 下列属性取值不为 nonetransformscalerotatetranslatefilterbackdrop-filterperspectiveclip-pathmask / mask-image / mask-border
  • isolation: isolate
  • will-change 指定了任何「非初始值时会创建上下文」的属性(如 transformopacity
  • containlayoutpaint,或含二者之一的复合值(strictcontent
  • 进入 top layer 的元素及其 ::backdrop,例如全屏元素与 popover
  • @keyframes 动画了会创建上下文的属性(如 opacity)且 animation-fill-mode: forwards

完整清单与示例见 MDN · Stacking context

警示 · 清单里有两条容易被误加:overflow: hidden 与不带 z-index 的 position: relative 都不会创建 stacking context——图 2-1 把它们也放进了开关里,可以看到徽标依旧浮在红条之上。但 overflow: hidden 会带来另一类问题(裁切),见三个常见排错场景。另一条相反的陷阱是 container-type:为了写容器查询给某个包裹层加上 container-type: inline-size,会顺手把它变成 stacking context,子元素的 z-index 就此被关在里面。

3 · isolation: isolate

上一节列出的触发条件里,大多数属性都附带改变视觉:opacity 会变透明、transform 会改变位置、filter 会改变像素。只有 isolation: isolate 是专门用于创建一个 stacking context、且不产生其他视觉效果的属性。

3.1 · 装饰用的 ::before 消失了

卡片想用伪元素 ::before 作背景装饰(渐变),让它压在文字下面、卡片背景上面,于是给它 z-index: -1。结果装饰消失了——因为卡片本身不是 stacking context 时,这个 -1 会落到整张卡片(连同它的白底)的下面,被白底遮住。

给卡片加一行 isolation: isolate,卡片就成为独立的 stacking context,-1 被限制在卡片内部,此时它在卡片白底之上、文字之下,装饰正常显现。

图 3-1 · 开关 isolation:isolate,看 z-index:-1 的装饰伪元素从白底之下浮回白底之上。

建议 · 同样能把 z-index: -1 限制在卡片内部的做法还有给卡片设 transformopacity: .999,但它们都带副作用:可能触发合成层、改变模糊与抗锯齿、影响 position: fixed 子元素的 containing block。isolation: isolate 表达的意图最明确——仅创建一个新的 stacking context,不做其他。

3.2 · 为组件划定边界

在设计系统里给可复用组件的根节点加 isolation: isolate,可以保证组件内部任意大的 z-index 都不会泄漏出去、压到组件外的元素;反过来外部的 z-index 也无法插入组件内部。整个组件成为一个 stacking order 可预测的独立 stacking context。

4 · 三个常见排错场景

几乎所有「z-index 拉到 9999 仍压不住」的问题,根因都相同:目标元素被限制在某个祖先的 stacking context 内部。下面三个是最常见的形态,每个都带修复开关。

4.1 · 被困的 Modal

Modal 设了 z-index: 9999,却被正文(z-index: 2)盖住——因为它是 z-index: 1 的 header 的子元素,被 header 这个 stacking context 限制在内部。

图 4-1 · 被 header 困住的 Modal。可切换修复开关,把 Modal 移到 header 之外(Portal)。

建议 · 除了 Portal,还有一条更彻底的路:把 Modal 送进 top layer<dialog>showModal() 打开、或给元素加 popover 属性并 showPopover(),浏览器会把它提到 top layer——那是一个位于整个文档之上的独立层,与它在 DOM 里嵌在多深的 stacking context 内部无关,z-index 也就不必再参与竞争。两条路都可用(popover 属性在 Chrome 114、Firefox 125、Safari 17 起可用,核对于 2026-08),区别是 top layer 由浏览器托管、不需要把节点搬家。

4.2 · 沉底的 Dropdown

导航栏里的下拉菜单 z-index: 100,被下方正文 z-index: 2 盖住——因为下拉被限制在 navbar(z-index: 1)内部,跨 stacking context 比较时只看 1 与 2。修法是把 navbar 的 z-index 提到正文之上。

图 4-2 · 沉底的 Dropdown。可切换开关提高 navbar 的 z-index,看整盒连同下拉一起浮上来。

4.3 · 被裁切的 Tooltip

Tooltip 设了 z-index: 1000,却被切掉一截——原因不是 stacking order,而是祖先的 overflow: hidden。裁切与 z-index 无关:再大的 z-index 也无法恢复被裁掉的像素。修法是去掉裁切,或把 tooltip 移出溢出容器。

图 4-3 · 被祖先 overflow:hidden 裁掉的 Tooltip。可切换开关验证调大 z-index 完全无效。

4.4 · 排错三步法

  1. 沿 DOM 向上逐层检查祖先,看是否有清单里那些会创建 stacking context 的属性。
  2. 找到把目标限制在内部的那个祖先后,四选一:提高该祖先的 z-index、重排 HTML 把目标移出去、用框架的 Portal 渲染到 <body> 下、或改用 top layer(showModal() 与 popover)。
  3. 怀疑是裁切而非 stacking order 问题时,检查祖先的 overflowclip-path

注 · 现成的排查工具有几样:Chrome 扩展「CSS Stacking Context Inspector」、VS Code 扩展「Better CSS Stacking Contexts」,以及 Edge 与 Firefox DevTools 的 3D / Layers 视图,都能直接标出页面上的 stacking context。

规范 / 文档