← 返回文章列表
CSS·1681 次阅读

彻底搞懂 position 定位与层叠上下文

上周帮同事看一个 bug:弹窗设置了 z-index: 9999,愣是被一个 z-index: 10 的侧边栏盖住。他满脸问号,我一看代码就明白了——弹窗的父元素是个 transform 元素,层叠上下文把它框死了。

这个坑我当年也踩过,今天把 position 和层叠上下文一次性捋清楚。

position 的五个值

先过一遍基础,重点是容易混淆的几个:

  • static:默认值,正常文档流,top/left 等偏移无效
  • relative:相对自己原来的位置偏移,不脱离文档流,原位置空间保留
  • absolute:相对最近的非 static 祖先定位,脱离文档流
  • fixed:相对视口定位,脱离文档流
  • sticky:relative 和 fixed 的混合体,滚动到阈值时"粘住"

最常见的错误:给 absolute 元素定位,结果参照物不对。

<div class="parent">
  <div class="child">我往哪飞?</div>
</div>

<style>
.parent {
  /* 忘了写这行,child 就相对 body 定位了 */
  position: relative;
  width: 300px;
  height: 300px;
}
.child {
  position: absolute;
  top: 20px;
  right: 20px;
}
</style>

记住一句话:absolute 找爸爸,爸爸必须非 static。一般做法就是给父元素加 position: relative,不产生偏移但能当参照物。

sticky 为什么有时候不粘

sticky 很好用,做吸顶导航一行搞定:

.nav {
  position: sticky;
  top: 0;
}

但它有两个经典失效场景:

  1. 父元素有 overflow: hidden / auto / scroll,sticky 的滚动参照被改了
  2. 父元素高度不够,sticky 的活动范围就是父元素的内容区,父元素和子元素一样高时根本没地方"粘"

我排查 sticky 失效的第一步永远是:往上翻父级,查 overflow 和高度。

层叠上下文:z-index 的真正规则

回到开头的 bug。z-index 不是全局比大小,它只在同一个层叠上下文里比较。

默认情况下,根元素(html)是唯一的层叠上下文,大家按 z-index 排队。但以下属性会创建新的层叠上下文:

  • position: relative/absolute/fixed 且 z-index 非 auto
  • position: sticky
  • opacity < 1
  • transform 非 none
  • filter 非 none
  • flex/grid 容器的子项 且 z-index 非 auto
  • will-change、isolation: isolate、mix-blend-mode 等

一旦创建了新的层叠上下文,这个元素连同它的所有后代就是一个整体,对外只用自己的 z-index 参与比较。

<div class="sidebar" style="position: relative; z-index: 10;">侧边栏</div>

<div class="wrapper" style="transform: translateX(0);">
  <!-- 弹窗 z-index 再大也没用,wrapper 整体对外没有 z-index(按 0 算) -->
  <div class="modal" style="position: fixed; z-index: 9999;">弹窗</div>
</div>

上面的结构里,wrapper 因为 transform 创建了层叠上下文,modal 的 9999 只在 wrapper 内部有效。对外比较的是 wrapper(z-index 按 auto 算)和 sidebar(z-index: 10),弹窗必输。

修复思路

知道原理后,解法就清晰了:

  1. 把弹窗挪到 body 直属层级,别让它被困在某个层叠上下文里——这也是为什么 Vue/React 组件库里 Modal 都用 Teleport/Portal 挂到 body 下
  2. 如果挪不动,就调整两个层叠上下文的"家长",让弹窗所在上下文的层级高过遮挡物
  3. 万不得已才动 z-index 数字本身,而且别搞 9999 vs 99999 的军备竞赛
<!-- 正确姿势:弹窗直接挂 body -->
<body>
  <div class="sidebar" style="position: relative; z-index: 10;">侧边栏</div>
  <div class="modal" style="position: fixed; z-index: 100;">弹窗,稳赢</div>
</body>

小结

  • absolute 找最近的非 static 祖先定位;sticky 失效先查父级 overflow
  • z-index 只在同一层叠上下文内比大小
  • transform、opacity < 1、filter 等都会创建层叠上下文,把后代的 z-index "框死"
  • 弹窗类组件直接挂 body,从结构上避开这类问题

下次再遇到"z-index 失灵",别急着加 9,先打开 DevTools 看看元素被哪个层叠上下文困住了。

看到这里了?如果这篇文章帮到过你

请我喝瓶可乐就好 🥤

收款码

评论0 条

还没有评论,来抢沙发~

发表评论

评论需审核后显示

© 2026 许冬博客 · 专注 JS 技术分享

·RSS 订阅·Powered by Nuxt 4