Skip to content

问题背景 ​

做一个卡片 hover 菜单时,菜单里有几个磨砂玻璃风格的按钮——backdrop-filter: blur(20px)。为了让菜单出现得更自然,父容器加了淡入 + 位移的过渡动画:

css
.menu {
  opacity: 0;
  transform: translateX(-20px);
  transition: opacity .3s ease, transform .3s ease;
}

.card:hover .menu {
  opacity: 1;
  transform: translateX(0);
}

按钮本身是这样的:

css
.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(不打算修)。

原文参考:

text
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 ​

css
.menu {
  opacity: 0;
  transform: translateX(-20px);
  transition: opacity .3s ease, transform .3s ease;
}

结果:有 Bug。 动画过程中毛玻璃消失,结束后闪回来。

Demo 2:父容器仅过渡 opacity ​

css
.menu {
  opacity: 0;
  transition: opacity .3s ease;
}

结果:有 Bug。 只要父容器带 opacity 过渡,子元素的 backdrop-filter 就会失效。

Demo 3:父容器仅过渡 transform ​

css
.menu {
  transform: translateX(-20px);
  transition: transform .3s ease;
}

结果:正常。 没有 opacity,毛玻璃效果全程稳定。

Demo 4:父容器只做 transform,子元素自己做 opacity ​

css
/* 父容器:只负责位移 */
.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,原来可能是这样:

html
<!-- 有 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>

改成这样:

html
<!-- 正确的写法:父容器只做 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 动画,大概率就是这个问题。解法也简单:拆开职责,让位移归父容器、透明度归子元素自己管。