先记住这个答案
position: fixed 正常情况下相对于视口定位,滚动页面时它始终固定在屏幕上。但如果它的某个祖先元素设了 filter,CSS 规范规定这个祖先会变成 fixed 元素的「包含块」。这时 fixed 实际上变成了相对于那个祖先定位,效果和 absolute 差不多——页面滚动时这个元素会跟着一起滚走。
filter可改变后代fixed的包含块- 效果等同于
fixed变成了absolute transform等属性也可能改变包含块- 把浮层移出滤镜父级可恢复视口定位
为什么 filter 会改变包含块
CSS 规范对 fixed 定位的包含块有一条特殊规则:如果某个祖先元素建立了新的包含块,fixed 就不再相对于视口定位。能建立新包含块的属性包括 transform、perspective、filter、backdrop-filter、will-change(当值是上述属性之一时),以及 contain: paint。
filter 之所以会这样,是因为浏览器对 filter 元素做了特殊的渲染处理——它需要先把元素和子元素画到一个离屏的画布上,再对画布做滤镜。这个离屏画布就是新的坐标系,fixed 定位的子元素在这个坐标系里计算位置,自然就不是相对于视口了。
这个行为和值无关。filter: blur(0px) 看起来什么都没做,但浏览器照样会走离屏渲染的流程,fixed 照样失效。
项目里怎么解决
最直接的办法是调整 DOM 结构,让 fixed 元素不在带 filter 的容器内部。
比如刚才的导航栏场景,把 <nav> 从 .page-wrapper 里挪出来,变成它的兄弟节点。灰度效果只加在 .page-wrapper 上,导航栏不受影响。
如果 DOM 结构改不了(比如在一个第三方组件里),可以考虑换个思路实现灰度效果。用 SVG filter 或者 mix-blend-mode 做覆盖层,就不会影响子元素的定位行为。
还有一种取巧的方式:如果只是想在整个页面上加灰度,把 filter 加在 <html> 元素上。<html> 是根元素,fixed 元素的包含块就是根元素本身,效果上和相对于视口定位是一样的。
容易答错的地方
- 以为是
z-index的问题 fixed元素没有固定住,第一反应经常是去调z-index。但包含块被改变之后,z-index再高也没用——元素的位置计算基准变了,它在视觉上就是在错误的位置。- 以为只有
transform会让fixed失效 transform是最广为人知的一个,但filter、backdrop-filter、perspective、will-change都有同样的效果。面试时只答transform而漏掉filter会显得理解不够全面。
面试官还会怎么问?
will-change: filter 也会让 fixed 失效吗?
会。will-change 的作用是告诉浏览器某个属性即将变化,浏览器会提前做准备——对于 filter 来说,准备工作就是创建离屏渲染环境,所以包含块同样会被改变。
filter 创建的层叠上下文和 z-index 创建的有什么区别?
效果一样,都是把子元素的层叠顺序限制在这个上下文内部。区别在于 z-index 需要配合 position: relative/absolute/fixed 才生效,而 filter 无条件创建——只要属性存在就行,不需要配合定位。
怎么检测页面上哪些元素创建了层叠上下文?
Chrome DevTools 的 Layers 面板可以看到所有合成层。3D View(在 More tools 里)可以看到层叠上下文的嵌套关系。也可以用 getComputedStyle 检查元素的 filter、transform、opacity 等属性是不是非初始值。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。