SCSS 的 $z-index-map 无法解决真实 DOM 层叠问题,因其仅在编译期生成静态值,不感知运行时 stacking context 变化,真正有效的是 CSS 自定义属性配合 JS 动态控制。

SCSS 的 $z-index-map 无法解决真实 DOM 层叠问题,它只是编译期生成静态值,对运行时的 stacking context 完全无感。
为什么 SCSS Map 管理 z-index 是伪需求
很多人用 SCSS map 存 z-index 值,比如:$z-index: (modal: 1000, dropdown: 900, toast: 800);,再配合 map-get($z-index, modal) 调用。这看起来很“工程化”,但本质仍是硬编码——所有值在 CSS 输出时就固定了,不感知父容器是否创建了新 stacking context,也不响应 JS 动态插入或 Portal 挂载带来的层级位移。
常见错误现象:
- 下拉菜单在 Modal 内打开,
z-index: 900却被 Modal 的z-index: 1000完全盖住(实际因 Modal 容器触发了 opacity 或 transform,子元素已无法越界) - 使用
createPortal把 Tooltip 挂到body下,但 body 没设position: relative,导致其 z-index 和页面其他 fixed 元素同级竞争,而别人用了z-index: 2147483647
真正起作用的是 CSS 自定义属性 + 运行时控制
必须把层级决策留在运行时,才能应对 Portal、动画、动态挂载等场景。CSS 自定义属性(--z-modal)可被 JS 读写,也能被浏览器原生解析为层叠依据。
立即学习“前端免费学习笔记(深入)”;
实操建议:
- 在
:root中定义语义化变量:--z-modal: 1000;、--z-toast: 800;,而非 SCSS 变量 - 组件内直接用:
z-index: var(--z-modal); - 需要临时提层时(如 hover 状态下拉),JS 动态改:
element.style.setProperty('--z-dropdown', '1050'); - 避免在 SCSS 中做数值计算(如
$z-modal + 10),那只会让调试和覆盖变得更难
Portal 类组件必须手动接管父容器 stacking context
Modal、Tooltip、Popover 这类通过 createPortal 或 teleport 挂载到 body 的组件,body 默认不是定位元素,不构成 stacking context。此时它的子元素 z-index 是和整个页面所有定位元素“平级”比大小的。
容易踩的坑:
- 没检查
body是否有position: relative或z-index,导致你的--z-modal: 1000被别人一个z-index: 1001覆盖 - 多个 Portal 组件共用同一个挂载点(如都挂
body),却没做 z-index 动态递增逻辑,结果后打开的 Modal 反而被先打开的盖住 - 动画开始前未提升 z-index,动画中 transform 触发新 stacking context,导致过渡过程出现层级闪烁
推荐做法:给 Portal 挂载点加一层 wrapper,并设 position: relative; z-index: 0;,确保它成为明确的 stacking context 根节点。
堆叠上下文才是真正的层级开关
绝大多数 z-index 失效,根本不是值设小了,而是元素被锁进了一个局部 stacking context。触发条件包括:
-
opacity小于 1(哪怕opacity: 0.99) -
transform不为none(包括translateZ(0)、scale(1)) -
filter、will-change、mask等属性 - 父元素设置了
z-index且position不为static
调试时优先打开浏览器开发者工具的「Computed」面板,搜索 stacking context ——如果目标元素旁边标着 “This element establishes a stacking context”,那就别再调子元素的 z-index 了,得动它爹。


















