
你可能也遇到过这种问题写了两三个图层明明给后面的元素设了高 z-index结果它还是被压住不动或者一个元素的 opacity 设成 0.5 之后原本子元素的层级全乱了套。这类问题的根源大部分不在 z-index 本身而在于对 CSS 层叠cascading和层叠上下文stacking context底层机制的理解不够。CSS 的层叠不是只看“数字大小”它是一套完整的规则决定了哪个元素应该覆盖在哪个元素上面。这篇内容我打算把层叠这块彻底拆开讲从最基础的层叠顺序到层叠上下文的创建和嵌套再到实操案例和调试技巧争取一次性把这块硬骨头啃掉。不管你是刚入门的前端新手还是写了几年 CSS 偶尔被坑一下的老人这篇都值得花十分钟看完。1. 层叠的基本功CSS 为什么会有“谁盖住谁”的问题1.1 层叠的本质多元素争抢同一块屏幕空间HTML 文档本身是流式的元素按照文档顺序从上到下、从左到右排列。但一旦你用了定位position、浮动float、Flexbox 或 Grid元素就可能脱离正常文档流在空间上发生重叠。重叠之后浏览器必须回答一个问题如果两个元素都画在同一个像素点谁显示谁隐藏CSS 给出的答案就是层叠机制。层叠系统里有两层核心逻辑层叠顺序stacking order在同一层叠上下文内元素按照一套固定规则按“优先级”排序。优先级低的下层先绘制优先级高的上层后绘制于是上层覆盖下层。层叠上下文stacking context一个独立的小世界内部元素的层叠顺序只在世界内部有效不能“穿透”去影响外面的元素。你可以把整个页面想象成一栋楼楼里有很多房间房间里有柜子柜子里有抽屉。层叠上下文就是“房间”层叠顺序就是“抽屉的上下层”。房间与房间之间是隔离的房间内部怎么摆都不影响别的房间。这个比喻在后面理解 z-index 失效问题时特别有用。1.2 同一层叠上下文内的优先顺序在一个层叠上下文内部元素的绘制顺序是有明确规定的。我以前一直记不住完整的规则后来自己总结成了一套方便记忆的“优先级阶梯”从底层到顶层依次是背景和边框最底层相当于画布的地基。负 z-index 的子元素跑到背景上方但还在普通内容下面。块级元素非定位文档流里的 div、p、section 等。浮动元素非定位带 float 的盒子堆在块级元素上方。行内元素非定位span、a、img 等再往上盖一层。z-index: 0 或 auto 的定位元素第一个真正“长在表面”的层。正 z-index 定位元素从 1 开始值越大越靠上。这套阶梯的核心是先画背景和负 z-index再画普通流内容最后画定位元素。很多人只知道 z-index 大就能盖住别人忽略了负 z-index 和元素类型的差异这就是很多“为什么我的定位元素盖不住背景”这类问题的根源。例如你给一个 div 设置了背景色又给另一个定位元素设了 z-index: -1那这个定位元素会被背景盖住不是它“失效”而是它排在了背景之后。提示position: static 且没有 z-index 的元素永远只能参与普通流层叠即使你写 z-index: 999 也不会生效因为 z-index 只对定位元素、flex/grid 子项生效。2. 层叠上下文CSS 里的“独立图层房间”2.1 哪些属性会创建层叠上下文层叠上下文是理解整个层叠体系的钥匙。一个元素一旦成为层叠上下文它就像有了一个独立的“容器”内部所有子元素的层叠关系都被限制在这个容器里不能对外面造成影响。最常见的触发条件包括根元素html天然是一个层叠上下文。position 设为 relative、absolute、fixed、sticky且 z-index 不为 auto。opacity 小于 1哪怕只有 0.99。transform 值不为 none比如 translate、scale、rotate。filter 值不为 none比如 blur、brightness。will-change 设置了上述任一属性。contain 值为 layout、paint、strict 等。mix-blend-mode 值不为 normal。isolation: isolate。flex/grid 子项且 z-index 不为 auto。看到这里你应该明白opacity: 0.5 或 transform: scale(1.2) 不是“没影响层级”它们直接创建了一个新的层叠上下文把子元素的 z-index 全部“锁”在里面。热搜词里“css涟漪光圈扩散”这类动画只要用到了 transform opacity 的过渡就会出现层叠上下文的变化后面我会专门用它做案例。2.2 层叠上下文的嵌套和“全局天花板”有了层叠上下文z-index 的数值就有了“局部性”。假设 A 是一个层叠上下文它的 z-index 是 5B 也是一个层叠上下文z-index 是 3。A 里面有一个子元素 a-childz-index 是 9999B 里面有一个子元素 b-childz-index 是 1。最终画面上谁在上面答案是 A 段位的 5 高于 B 段位的 3所以 A 里面的所有内容包括 z-index: 9999 的 a-child都会盖在 B 里面所有内容之上哪怕 b-child 设到 99999 也盖不过去。这就是被无数人误解的“全局天花板”z-index 并不是全页面统一排大小的它是一个树状的局部系统。每个层叠上下文都是独立的比较单位。子元素的 z-index 再大也不能超过它父亲所在的“段位”。我在项目里遇到过最典型的例子页面上有个固定侧边栏 z-index: 100结果侧边栏里有个弹出下拉菜单设了 z-index: 9999谁想怎么都够高了偏偏被某个外层 transform 容器压住。原因是那个外层容器本身 z-index 比较低下拉菜单再大也只能在容器内部称王。我给你画一个简单的层级关系表方便理解父级上下文父级 z-index子级 z-index实际表现父A 上下文5子a: 9999全部在 B 之上父B 上下文3子b: 1全部在 A 之下3. z-index 实战从数值规则到常见误用3.1 z-index 的合法值和生效前提z-index 的取值很清晰auto不新建层叠上下文定位元素但 z-index 为 auto 时按顺序层叠。整数包括负值数值越大越靠上。全局值inherit、initial、unset。但 z-index 能生效的前提有三个缺一不可元素必须是定位元素position: relative/absolute/fixed/sticky或者 flex/grid 子项。元素的 position 不是 static。元素位于同一个层叠上下文内比较这一点很多人忽略。如果你给一个 position: static 的 div 设 z-index: 9999什么都不会发生浏览器直接忽略它。我见过不少初学同学在这个地方卡很久总觉得代码写错了其实只是没定位。另外现在的 flex 布局很普及flex 子项即使不定位只要有 z-index 就可以参与层叠比较这是和传统块级元素不太一样的地方具体使用时可以留意。3.2 负 z-index 的“地下室”效果负 z-index 不是没用它能让元素跑到背景和内容之间形成“元素在背景之上、在文本之下”的效果。比如做一个卡片卡片本身有背景色伪元素设置 z-index: -1 可以用来绘制“卡片背后的阴影”甚至“背后的装饰大图”这样阴影就不会盖住卡片内容效果干净利落。这里的一个坑是如果这个卡片的父级没有创建层叠上下文负 z-index 元素可能直接跑到页面根层叠上下文里盖住别的元素。我以前做卡片阴影时发现阴影居然跑到页面其他区域去了折腾半天才意识到父级缺少隔离。解决思路很简单给卡片容器加上 isolation: isolate 或 transform: scale(1) / opacity: 0.99强制它成为一个层叠上下文把负 z-index 的子元素关进“笼子”。3.3 为什么 z-index: 9999 也压不过别人你很可能在项目里见过这种“世界级”常量z-index: 99999。然后有一天它失效了。失效原因通常是以下某一种元素不在同一个层叠上下文里比如它父级创建了上下文且父级段位低。元素没有定位这个问题最常见。祖先元素创建了层叠上下文祖先设置了 opacity、transform、filter 等。兄弟元素之间的层叠结构z-index 只在兄弟之间比较父子之间不直接比较。排查方法很简单打开浏览器 DevTools选中元素查看 Computed 面板里的 z-index 是否生效再逐级往上查看是否有 opacity、transform、filter 等属性。往往顺着这条线几分钟就能定位问题。注意z-index: 0 和 z-index: auto 不一样。定位元素 z-index: 0 会创建一个层叠上下文而 auto 不创建。这对 CSS 性能优化和隔离策略很重要。4. 用层叠机制实现常见视觉特效涟漪、光圈、流光边框4.1 涟漪光圈扩散效果怎么做才不“串层”热搜词里的“css涟漪光圈扩散”本质上是点击某个元素时在点击位置生成一个随着时间扩大的圆圈光圈。通常实现方式是利用伪元素或额外子元素设置初始宽高为 0再通过 transform: scale 放大 opacity 淡出最后动画结束后移除。但这里有个非常隐秘的层叠问题光圈本身是在元素上方漂浮的如果元素内部有文字或图片内容光圈必须保证不遮挡文字交互也不能落到背景后面去。我建议的做法是给“涟漪容器”创建一个独立的层叠上下文例如在按钮上设置 isolation: isolate然后在涟漪子元素里设置 z-index: -1让它浮在按钮背景上、内容下面。这样一来涟漪不会覆盖按钮文字文字仍然清晰可读。涟漪也不会跑到按钮背景后面因为负 z-index 只在按钮这个上下文内部生效。其他页面元素不会受到涟漪的层叠影响。实现时动画可以这样控制先用 JavaScript 获取点击坐标再插入一个 span宽高从 0 到最大边缘值同时 opacity 从 0.5 减到 0transition 持续时间取 600ms 左右。CSS 部分核心代码如下.ripple-container { position: relative; overflow: hidden; isolation: isolate; } .ripple { position: absolute; border-radius: 50%; background: rgba(255, 255, 255, 0.6); transform: scale(0); animation: ripple-animation 0.6s ease-out forwards; z-index: -1; } keyframes ripple-animation { to { transform: scale(1); opacity: 0; } }在动画执行时要注意 transform 会创建层叠上下文动画结束如果删除了元素不会残留上下文但动画过程中如果父元素里有其他定位元素要考虑是否会因为临时上下文导致闪烁。我在实际项目里遇到过动画过程中按钮上的图标抖动就是因为动画期间产生了 transform 上下文把其他元素挤到了新的层叠关系里。最后的解法是给动画目标加一层单独的 wrapper互不干扰。4.2 流光边框与动态背景的层级控制“流光边框”通常用 background-clip: text、渐变背景动画或外圈光晕实现。表面上看起来和层叠没多大关系但其实它涉及背景绘制顺序和伪元素的层级问题。流光边框的经典做法是在元素外层加一个伪元素背景是渐变色然后让渐变的 background-position 不断滚动再用 mask 或 border-box 遮罩只露出边框部分。伪元素默认在元素自身的背景之上还是在之下答案是如果伪元素是普通定位的子元素它渲染在父元素的背景之上、内容之下但如果父元素自己有 z-index 或创建了层叠上下文这个关系可能变化。我通常在流光边框上使用两层结构第一层是“边框背景层”作为父元素的伪元素或背景层z-index: 0只负责渐变流动。第二层是“内容层”z-index: 1承载文字或图片确保视觉上内容永远在流光之上。这种做法让内容不会被流动的渐变遮住而且视觉上流光像从元素边缘“透”出来的。另外一个经验如果流光边框的元素要悬浮在其他元素上面一定要给整个元素设置一个明确的 z-index 和层叠上下文否则伪元素的负 z-index 会跑出边界流光可能会盖住页面其他不该被盖住的内容。4.3 植物大战僵尸布局中的“层叠规划”思路热搜词里有个“植物大战僵尸 html 完整代码”听起来和 CSS 层叠没什么关系但如果你真去实现这个游戏会发现它是最考验层叠规划的场景之一。地图像一个棋盘每个格子都可能同时存在植物、僵尸、子弹、阴影、特效。植物和植物之间会重叠吗会有些植物动画会把叶片伸出格子。子弹的飞行轨迹会穿过多个格子要在植物和僵尸之间飞行。僵尸倒下时动画也要叠在植物身上。合理的层叠规划是背景地块绘制在最低层z-index: 0。植物层z-index: 10按行设置基础层级。僵尸层z-index: 20僵尸要踩在植物前面。子弹层z-index: 30子弹要显得在飞行中。特效层z-index: 40比如爆炸、阳光闪烁。HUD 界面z-index: 100永远在最上面。然后每行内部再做一个层叠上下文隔离避免不同行之间的层级冲突。这个思路和普通业务系统的“弹窗层、遮罩层、导航层”规划如出一辙。只要规划好代码写起来会非常顺畅不需要到处设置 9999。5. 层叠调试技巧与常见问题排查实录5.1 用 DevTools 可视化层叠上下文Chrome 和 Edge 的 DevTools 现在有一个很好用的功能选中任意元素切到 Elements 面板右侧选择“Layout”或“层叠上下文”视图可以看到该元素所在的层叠上下文树。这比自己一层层查 style 要直观得多。我调试层叠问题时通常的流程是选中出问题的元素看它的 z-index 是否为预期值。看它的祖先链中是否有 opacity、transform、filter、will-change 等属性。如果有层叠上下文视图直接在视图里看当前元素位于哪个上下文之下它的“相对层级”是多少。对比同上下文内其他兄弟元素的 z-index找出谁更大。找到原因后我会优先考虑“调整父级上下文”而不是无脑继续加 z-index。比如一个弹窗死活盖不住遮罩但遮罩的父容器创建了层叠上下文那我直接把弹窗移到遮罩容器的同级通常比调数字更干净。5.2 一张表理清高频层叠问题现象常见原因解决方案z-index: 9999 但被低 z-index 元素盖住祖先创建了层叠上下文本元素段位低于对方给父级加 isolation: isolate或调整父级层级负 z-index 元素跑到页面其他角落父级未创建层叠上下文父级加 transform/opacity/isolation弹窗盖不住遮罩弹窗在遮罩的上下文内部比较维度不对把弹窗提升到遮罩同级容器opacity 动画结束后子元素层级异常opacity 变化创建了层叠上下文动画结束后移除 opacity或用 visibility 代替固定定位元素位置对但层级低fixed 元素祖先有 transform把 fixed 元素移出 transform 容器flex 子项 z-index 失效没有设置定位或者父级未创建上下文明确 position 或检查父级上下文5.3 项目里的层叠规范建议踩过足够多的坑以后我建议每个团队在项目里定一份简单的层叠规定不用太复杂核心是“分档”而不是“一个比一个大”。比如基础内容层z-index 不设或者 auto。浮层组件提示、冒泡z-index: 100。固定导航栏/头部z-index: 200。弹窗内容z-index: 300。遮罩层z-index: 299比弹窗略低。全局通知toast、全局加载z-index: 1000。同时规定任何动画属性transform、opacity、filter如果用在容器上要评估是否会产生层叠上下文必要时用 isolation: isolate 显式隔离。这个规范看起来简单但能避免 90% 的层级混乱问题。写在最后我个人在实际操作中的体会是CSS 层叠不是一个需要背的公式而是一套需要建立“场景感”的规则。每次碰到层级问题不要急着加 z-index先画一下元素的“房间结构”谁和谁是同一个房间谁在房间外这两个房间谁的地基更高。想清楚这个大部分问题一眼就能看穿。上面提到的涟漪光圈、流光边框、植物大战僵尸布局这些案例都是我在实际项目里反复用到层叠机制的典型场景希望这些经验能帮你少走一点弯路。最后再分享一个小技巧如果你不确定某个属性是否创建了层叠上下文最快的验证方式是在 DevTools 的 Computed 面板里找到 z-index 的值——如果它显示为具体数字而不是 auto那就说明这个元素很可能已经是层叠上下文了。