结论:仅用css变量定义z-index值无效,必须配合position显式设置、作用域隔离、数值留空档及portal挂载点定位接管。原因包括:static元素忽略z-index;父容器触发新层叠上下文;body未定位导致平级比较;scoped样式使变量失效;跨端框架中变量作用域受限;共用变量引发层级冲突;calc嵌套兼容性差;js读取需类型转换;portal需手动建立定位上下文。

直接说结论:只靠 CSS 变量(--z-modal)定义数值远远不够,必须配合 position 显式设置、作用域隔离、数值留空档,以及 Portal 类组件的挂载点定位接管——否则变量写得再整齐,也大概率不生效。
为什么 z-index: var(--z-modal) 写了却盖不住其他元素
90% 的失效不是变量没加载,而是目标元素根本没进入层叠上下文:
-
position仍是static(默认值),z-index完全被忽略;打开开发者工具看「Computed」面板,如果z-index显示为auto或灰掉,就是这个原因 - 父容器用了
transform、opacity: 0.99、filter等属性,悄悄创建了新层叠上下文,子元素的z-index只能在它内部比大小 - 第三方组件(如
uni-modal)通过createPortal挂到body下,但body默认没设position,导致它的子元素和全站所有fixed元素“平级”比大小,而别人写了z-index: 2147483647
变量必须在非 scoped 的顶层样式中声明
:root { --z-modal: 1000; } 写在 <style scoped></style> 里是无效的——scoped 会给 :root 加属性选择器,把它锁死在当前组件内,其他地方读不到。
- 真正全局生效的位置只有一处:项目最顶层的非
scoped样式文件中(如 Vue 的App.vue里的<style></style>,或 React 的index.css) - 跨端框架(如 UniApp)要特别注意:
uni.showToast渲染的 DOM 在body下,如果变量只定义在某个 page 文件里,它完全看不到 - 禁止把所有变量塞进
:root——--z-modal被多个组件共用,某天弹窗升级到1200,侧边栏菜单就可能意外浮到最前
数值设计必须留间隙、按功能域隔离
相邻层级至少差 10,中间预留插入空间;不同功能模块用不同变量名,哪怕初始值相同。
- 推荐结构:
--z-tooltip: 300、--z-dropdown: 900、--z-modal: 1000、--z-fullscreen: 1500——中间空出足够余量 - 模态框内部可就近定义:
.modal { --z-element: var(--z-modal, 1000); z-index: var(--z-element); },下拉菜单则用--z-dropdown,彼此隔离 - 禁用
calc(var(--z-base) + 10)嵌套——旧版 Safari 对calc+var()支持不稳定 - JS 动态读取时,
getComputedStyle().getPropertyValue('--z-modal')返回的是字符串,比较前必须转数字:parseInt(zIndexStr, 10) > 900
Portal 类组件必须手动接管挂载点的定位
Modal、Tooltip 这类挂到 body 的组件,默认 body 不是定位元素,不构成 stacking context。它们的子元素会和全站所有定位元素“平级”比大小。
- 挂载后检查并设置:
document.body.style.position = 'relative'(若未设) - 更稳妥的做法是创建专用容器:
<div id="overlay-root" style="position: relative"></div>,所有 Portal 统一挂载到这里 - 多个 Portal 共用一个挂载点时,需 JS 动态递增
--z-modal,避免后挂载的覆盖先挂载的
最常被忽略的一点:变量只是数值容器,真正决定层级是否“活起来”的,是那个被很多人随手删掉的 position: relative —— 它不在变量里,也不在编译时校验,只在运行时悄悄起效。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











