问题背景
做一个卡片 hover 菜单时,菜单里有几个磨砂玻璃风格的按钮——backdrop-filter: blur(20px)。为了让菜单出现得更自然,父容器加了淡入 + 位移的过渡动画:
.menu {
opacity: 0;
transform: translateX(-20px);
transition: opacity .3s ease, transform .3s ease;
}
.card:hover .menu {
opacity: 1;
transform: translateX(0);
}按钮本身是这样的:
.btn {
background: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
}预期效果:菜单出现的过程中,按钮始终带着毛玻璃模糊效果,整体跟着淡入滑出。
实际效果: 动画过程中按钮是透明背景,完全没有模糊,等过渡动画结束的一瞬间,backdrop-filter 才突然生效,"闪"出来。
排查过程
第一反应是自己哪里写错了,翻来覆去检查了 z-index、position、will-change,都没问题。
然后搜到了 Chrome 的已知 Bug:issuetracker.google.com/issues/365917970
结论:当一个元素的祖先节点存在 opacity 动画/过渡时,该元素的 backdrop-filter 在动画期间不会正确渲染。 动画结束后才会突然生效,就是那个"闪一下"的视觉 Bug。
这个 Issue 标注的状态是 Won't Fix(不打算修)。
原文参考:
Status: Won't Fix (Intended Behavior)
Your example is showing expected stacking behavior. When .container has opacity:1, it is not a stacking context, so .under, which is position:absolute, paints into the root stacking context. .over is a stacking context and paints after the root stacking context, so it occludes .under.
When .container gets opacity<1, it becomes a stacking context so .under and .over both paint into it. According to the specification for paint order, .under is painted after .over because it's a positioned descendant. That's why .under isn't blurred.
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context
https://www.w3.org/TR/CSS2/zindex.html#painting-order复现 & 对比
为了搞清楚具体是哪种情况触发、哪种情况正常,我做了四个对比 Demo,背景都是彩色渐变 + 格子底图,方便肉眼看模糊效果:
Demo 1:父容器同时过渡 opacity + transform
.menu {
opacity: 0;
transform: translateX(-20px);
transition: opacity .3s ease, transform .3s ease;
}结果:有 Bug。 动画过程中毛玻璃消失,结束后闪回来。
Demo 2:父容器仅过渡 opacity
.menu {
opacity: 0;
transition: opacity .3s ease;
}结果:有 Bug。 只要父容器带 opacity 过渡,子元素的 backdrop-filter 就会失效。
Demo 3:父容器仅过渡 transform
.menu {
transform: translateX(-20px);
transition: transform .3s ease;
}结果:正常。 没有 opacity,毛玻璃效果全程稳定。
Demo 4:父容器只做 transform,子元素自己做 opacity
/* 父容器:只负责位移 */
.menu {
transform: translateX(-20px);
transition: transform .3s ease;
}
/* 子按钮:自己负责淡入,同时保有 backdrop-filter */
.btn {
background: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
opacity: 0;
transition: opacity .25s ease;
}
.card:hover .menu { transform: translateX(0); }
.card:hover .btn { opacity: 1; }结果:正常。 backdrop-filter 全程生效,淡入动画也有。
根本原因
Chrome 在做合成(compositing)时,当一个元素的 opacity < 1 且处于动画状态,它会把整个子树作为一个独立的位图来处理,再统一应用透明度。而 backdrop-filter 依赖的是"读取当前背后内容"这个实时操作,在位图化之后就失去了这个能力,于是 blur 就消失了。
用一句话概括就是:opacity 动画在祖先节点上,会把子树"拍平"成位图,导致子节点的 backdrop-filter 失去读取背景的能力。
解决方案
把 opacity 的责任从父容器移到子元素本身,父容器只做不影响合成的 transform 动画。
对应到 Tailwind,原来可能是这样:
<!-- 有 Bug 的写法:父容器同时控制 opacity 和 transform -->
<div class="... opacity-0 -translate-x-4 transition-all group-hover:opacity-100 group-hover:translate-x-0">
<button class="backdrop-blur-[20px] ...">Image to 3D</button>
<button class="backdrop-blur-[20px] ...">Image to Video</button>
</div>改成这样:
<!-- 正确的写法:父容器只做 transform,子元素自己做 opacity -->
<div class="... -translate-x-4 transition-transform group-hover:translate-x-0">
<button class="backdrop-blur-[20px] opacity-0 transition-opacity group-hover:opacity-100 ...">Image to 3D</button>
<button class="backdrop-blur-[20px] opacity-0 transition-opacity group-hover:opacity-100 ...">Image to Video</button>
</div>关键点:
- 父容器的
transition里不能有opacity - 需要淡入效果的话,让带
backdrop-filter的元素自己声明opacity过渡
小结
这个 Bug 很隐蔽,因为单独看 backdrop-filter 是好的,单独看 opacity 过渡也是好的,只有两者叠加(父有 opacity 动画、子用 backdrop-filter)才会触发。
遇到 backdrop-filter 失效的场景,先检查祖先链上有没有 opacity 动画,大概率就是这个问题。解法也简单:拆开职责,让位移归父容器、透明度归子元素自己管。