导语: 你是否在使用 NProgress
时,遇到过进度条神秘消失在某些元素下方?调整z-index
无效?本文通过一个真实案例,带你深入CSS 层叠上下文原理
,直击问题根源,并提供一针见血的解决方案!
一、诡异现象:进度条层级为何突然失效?
问题描述: 当调用NProgress.done()
时,进度条淡出动画部分被活跃 Tab
遮挡。
(为了方便观察,我通过一个 Demo 来演示问题现象,如下GIF图)
问题分析过程:
1. Tab 3(当前激活的Tab
)被遮挡,是不是激活状态下层级过高?
不是。因Tab
默认状态下,其背景颜色是透明的;如果背景是非透明颜色,同样存在遮挡问题。
2. 进度条是在 NProgress.done
之后失效,是否与 NProgress.done
逻辑相关?
先抛出问题结论:是的。具体原因我们继续往下分析;
首先,我们需要深入 NProgress
源码,看看 NProgress.done
都干了些什么?
JS
NProgress.done = function(force) {
if (!force && !NProgress.status) return this;
return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
};
NProgress.done
将进度设置为 100%
,我们再深入看看其中具体执行了什么逻辑?
深入 inc
和 set
方法,inc
方法可以让动画过渡更自然,这显然不会产生层叠的问题;
JS
NProgress.inc = function(amount) {
var n = NProgress.status;
if (!n) {
return NProgress.start();
} else {
if (typeof amount !== 'number') {
amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95);
}
n = clamp(n + amount, 0, 0.994);
return NProgress.set(n);
}
};
我们再深入看看 set
方法,选取关键片段(省略部分无关代码):
JS
NProgress.set = function(n) {
...
queue(function(next) {
...
if (n === 1) {
css(progress, { transition: 'none', opacity: 1 });
progress.offsetWidth; /* Repaint */
setTimeout(function() {
css(progress, { transition: 'all ' + speed + 'ms linear', opacity: 0 });
...
}, speed);
}
...
});
return this;
};
根据以上代码片段,当 n === 1
时,也就是执行 Nprogress.done
时,设置进度条的淡出效果。
通过 opacity: 1
-> opacity: 0
实现,问题开始浮现了。
接下来,我们先看看 NProgress
的 HTML
结构和部分 CSS
样式。
上面的progress
就是 NProgress
的根元素, HTML
模板的结构如下:
HTML
<div id="nprogress">
<div class="bar" role="bar">
<div class="peg"></div>
</div>
<div class="spinner" role="spinner">
<div class="spinner-icon"></div>
</div>
</div>
下面是部分 CSS
样式,其中元素的 z-index
的设置在 bar
元素上;
CSS
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: #29d;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
❌ 看似合理的代码,却暗藏玄机!
二、层叠上下文原理大揭秘
1. 什么是层叠上下文?
浏览器将元素划分到不同的"层级集团"(层叠上下文),同一集团内通过z-index
决斗,不同集团则按父级层级排序。
2. opacity的隐藏特性
熟读 W3C 规范
的同学都知道,W3C 规范
明确指出,当元素 opacity < 1
时,会创建层叠上下文。
也就是说此时无论 bar
的 z-index
多高,当opacity < 1
时,其层级都只在 progress
中有效,无法突破到更高层级的上下文集团。
二、解决方案:
方案一:使用 rgba 替代 opacity (源码修改)
CSS
#nprogress .bar {
background: rgba(34, 153, 221, 1)
}
JS
NProgress.set = function(n) {
...
queue(function(next) {
...
if (n === 1) {
css(progress, { transition: 'none', background: 'rgba(34, 153, 221, 1)' });
progress.offsetWidth; /* Repaint */
setTimeout(function() {
css(progress, { transition: 'all ' + speed + 'ms linear', background: 'rgba(34, 153, 221, 0)' });
...
}, speed);
}
...
});
return this;
};
原理: 通过rgba
实现透明度,避免触发新的层叠上下文。
方案二:层级升级(样式覆盖)
CSS
#nprogress {
position: relative;
z-index: 1031; /* 提升整个容器层级 */
}
原理:在父级建立高优先级层叠上下文,提升整个容器的层级。
三、防坑指南
四、原理延伸:CSS 层叠上下文
定义
层叠上下文(Stacking Context) 是 CSS 中用于管理元素在 Z 轴上堆叠顺序的三维概念。浏览器将页面元素划分为不同的"层级集团",同一集团内的元素通过 z-index
决定堆叠顺序,不同集团则根据父级层叠上下文的层级关系排序。
简单来说,层叠上下文是一个独立的渲染层级,内部的子元素受限于该层级的规则,无法直接与其他层叠上下文中的元素比较层级高低。
特性
-
独立性 每个层叠上下文与其兄弟元素完全独立,内部子元素的堆叠顺序仅在该上下文中有效。例如:
CSS.parent { position: relative; z-index: 1; } /* 创建层叠上下文 */ .child { z-index: 9999; } /* 仅在 .parent 内部有效 */
-
自包含性 层叠上下文内的所有子元素被视为一个整体,在父级上下文中按层级顺序堆叠。
-
层级继承性 子元素的堆叠等级受父级层叠上下文限制。例如,即使子元素的
z-index
很高,若父级层级低,子元素也无法覆盖其他高层级父级中的元素。
触发条件
以下属性会触发新的层叠上下文:
-
根元素 :
<html>
默认创建根层叠上下文。 -
定位元素 :
position
值为absolute/relative/fixed/sticky
且z-index
不为auto
。 -
CSS3 属性:
opacity < 1
transform
不为none
filter
不为none
flex
或grid
容器的子元素(z-index
非auto
时)isolation: isolate
will-change
指定了相关属性(如opacity
、transform
)