CSS 实现渐变毛玻璃:从 backdrop-filter 到多层 mask
在很多现代网页中,底部工具栏、悬浮导航、弹窗和卡片并不是一块完全不透明的背景。它们会保留后方内容的颜色和轮廓,同时把细节柔化掉,看起来像一块半透明的磨砂玻璃。
普通毛玻璃的模糊强度是均匀的,整个区域看起来像盖了一层相同厚度的雾。渐变毛玻璃则不同:模糊会沿着某个方向逐渐增强或减弱,边界更柔和,也更适合做页面底部的过渡遮罩。
例如,一个固定在页面底部的渐变毛玻璃栏,可以让靠近内容区的位置仍然比较清晰,越接近底部越模糊。这样既能让内容自然地过渡到工具栏,又不会突然出现一条明显的白色分界线。
常见使用场景包括:
- 页面底部固定操作栏、返回按钮或聊天输入区;
- 顶部导航和悬浮工具栏;
- 图片或渐变背景上的浮层卡片;
- 需要弱化背景、但又不希望完全遮挡背景的弹窗;
- 滚动内容与固定区域之间的视觉过渡。
这篇文章只讨论渐变毛玻璃,不讨论完整的液态玻璃材质。核心问题也很明确:如何让 backdrop-filter 的模糊效果沿一个方向平滑变化?
一、普通毛玻璃为什么不够
最常见的毛玻璃写法是:
css
.glass {
background: rgb(255 255 255 / 20%);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
backdrop-filter 会处理元素背后的内容。blur(12px) 表示整个元素区域使用同一个模糊半径,因此它得到的是均匀的毛玻璃。
这种效果适合面积较小的卡片、弹窗和按钮。但如果它被用在一个高度较大的底部区域,就容易出现两个问题:
- 玻璃区域的顶部和底部模糊程度完全一样,层次感比较弱;
- 玻璃区域和页面内容之间像是硬切了一刀,过渡不够自然。
直觉上,我们可能会尝试这样写:
css
.glass {
backdrop-filter: blur(0) blur(12px);
}
但这并不会产生从 0px 到 12px 的渐变。blur() 是滤镜函数,不是可以直接做插值的背景属性;同一个元素上的多个 blur() 也不是"前面小、后面大"的空间渐变。
要实现渐变毛玻璃,需要把问题拆成两部分:
- 用不同元素提供不同强度的模糊;
- 用蒙版控制每个模糊层在哪些位置显示。
二、渐变毛玻璃的基本原理
一个简单的渐变毛玻璃可以拆成下面几层:
text
背景内容
↓
轻度模糊层:覆盖范围最大
↓
中度模糊层:覆盖范围变小
↓
重度模糊层:只覆盖目标区域
↓
内容和交互层
每个模糊层都使用 backdrop-filter,但模糊半径不同。例如:
css
.layer-1 {
backdrop-filter: blur(1px);
}
.layer-2 {
backdrop-filter: blur(3px);
}
.layer-3 {
backdrop-filter: blur(5px);
}
.layer-4 {
backdrop-filter: blur(7px);
}
如果四层都完整显示,效果会变得过于厚重。于是再给每层加一个垂直渐变蒙版:
css
.layer-1 {
mask: linear-gradient(to bottom, #000 0 85%, transparent 100%);
}
.layer-2 {
mask: linear-gradient(to bottom, #000 0 61%, transparent 81%);
}
.layer-3 {
mask: linear-gradient(to bottom, #000 0 36%, transparent 56%);
}
.layer-4 {
mask: linear-gradient(to bottom, #000 0 12%, transparent 32%);
}
这样一来,轻度模糊覆盖更大的范围,重度模糊只在局部出现,最终叠加出逐渐增强的模糊效果。
这里的 #000 不是黑色背景,而是蒙版中的"完全显示";transparent 表示"完全隐藏"。渐变中间的灰度区域则表示按透明度显示这一层。
所以,渐变毛玻璃并不是让一个 blur() 自身渐变,而是:
text
不同 blur 半径的多层结果
+
每层不同范围的 mask
=
空间上逐渐变化的模糊效果
三、从 FrostedSurface 看多层接力
当前组件的实现正是这个思路。它准备了四组配置:
js
const FROSTED_LAYER_CONFIG_LIST = [
{ blur: '1px', solidEnd: '85%', transparentEnd: '100%' },
{ blur: '3px', solidEnd: '61%', transparentEnd: '81%' },
{ blur: '5px', solidEnd: '36%', transparentEnd: '56%' },
{ blur: '7px', solidEnd: '12%', transparentEnd: '32%' },
];
每组配置包含两个信息:
blur:当前层的模糊半径;solidEnd和transparentEnd:当前层从完全显示过渡到完全透明的位置。
组件再把配置转换为类似下面的 CSS 变量:
css
.frosted-layer {
--blur: 5px;
--mask: linear-gradient(
to bottom,
#000 0%,
#000 36%,
transparent 56%
);
backdrop-filter: blur(var(--blur));
-webkit-backdrop-filter: blur(var(--blur));
-webkit-mask: var(--mask);
mask: var(--mask);
}
这种设计有一个重要特点:相邻层的蒙版过渡区是重叠的。
例如,第二层在 61% 到 81% 之间淡出,第三层在 36% 到 56% 之间淡出。各层不是在同一条位置突然切换,而是通过重叠的透明度变化逐步接力。
如果把每层的蒙版写成完全不重叠的区间,就可能出现明显的横向分界线:某一层刚消失,下一层还没有完全出现,画面会出现断层。
模糊方向
渐变毛玻璃可以向上渐变,也可以向下渐变。改变方向时,通常只需要改变蒙版的方向:
css
/* 从上到下逐渐淡出这一层 */
mask: linear-gradient(to bottom, #000 0 60%, transparent 100%);
/* 从下到上逐渐淡出这一层 */
mask: linear-gradient(to top, #000 0 60%, transparent 100%);
不要为每个方向复制一套层配置。更好的做法是让方向成为一个独立参数,所有模糊层复用同一方向,只保留各层不同的模糊半径和停止位置。
四、一个可以直接运行的 HTML 示例
下面的示例包含完整的 HTML 和内嵌 CSS。把它复制到任意 HTML 文件中即可看到效果。页面背景使用了几个彩色圆形,方便观察玻璃后方内容是如何逐渐变模糊的。
完整 HTML
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>渐变毛玻璃 Demo</title>
<style>
:root {
color: #f7f9ff;
background: #11182b;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html {
min-height: 100%;
background: #11182b;
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
}
.demo-page {
position: relative;
min-height: 1800px;
overflow: hidden;
padding: 72px clamp(24px, 7vw, 120px) 420px;
background:
linear-gradient(rgb(255 255 255 / 7%) 1px, transparent 1px),
linear-gradient(90deg, rgb(255 255 255 / 7%) 1px, transparent 1px),
radial-gradient(circle at 16% 12%, #fa558d 0 7%, transparent 23%),
radial-gradient(circle at 85% 33%, #407eff 0 8%, transparent 24%),
radial-gradient(circle at 42% 78%, #ffbd4a 0 7%, transparent 22%),
linear-gradient(135deg, #10162b, #273965 52%, #10182d);
background-size: 48px 48px, 48px 48px, auto, auto, auto, auto;
}
.demo-header {
position: relative;
z-index: 1;
max-width: 720px;
}
.demo-kicker {
margin: 0 0 18px;
color: #96b9ff;
font-size: 12px;
font-weight: 700;
letter-spacing: .18em;
}
h1 {
max-width: 680px;
margin: 0;
font-size: clamp(48px, 9vw, 108px);
letter-spacing: -.06em;
line-height: .92;
}
.demo-header p {
max-width: 560px;
margin: 28px 0 0;
color: rgb(247 249 255 / 70%);
font-size: 18px;
line-height: 1.7;
}
.background-content {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
margin-top: 180px;
}
.background-card {
min-height: 220px;
padding: 24px;
border: 1px solid rgb(255 255 255 / 18%);
border-radius: 20px;
background: rgb(255 255 255 / 10%);
}
.background-card strong {
display: block;
margin-bottom: 54px;
color: rgb(255 255 255 / 88%);
font-size: 42px;
line-height: 1;
}
.background-card span {
color: rgb(255 255 255 / 64%);
font-size: 14px;
line-height: 1.5;
}
.background-line {
position: absolute;
top: 620px;
right: 8%;
left: 8%;
height: 1px;
background: rgb(255 255 255 / 54%);
transform: rotate(-8deg);
}
.frosted-footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
height: 280px;
overflow: hidden;
border-top: 1px solid rgb(255 255 255 / 58%);
background: rgb(255 255 255 / 5%);
isolation: isolate;
pointer-events: none;
}
.frosted-layer {
position: absolute;
inset: 0;
background: rgb(255 255 255 / 5%);
pointer-events: none;
-webkit-backdrop-filter: blur(var(--blur));
backdrop-filter: blur(var(--blur));
-webkit-mask: var(--mask);
mask: var(--mask);
}
/* 与 FrostedSurface 的 frosted-down 方向一致:越靠近底部越模糊。 */
.frosted-layer-1 {
--blur: 1px;
--mask: linear-gradient(to top, #000 0%, #000 85%, transparent 100%);
}
.frosted-layer-2 {
--blur: 3px;
--mask: linear-gradient(to top, #000 0%, #000 61%, transparent 81%);
}
.frosted-layer-3 {
--blur: 5px;
--mask: linear-gradient(to top, #000 0%, #000 36%, transparent 56%);
}
.frosted-layer-4 {
--blur: 7px;
--mask: linear-gradient(to top, #000 0%, #000 12%, transparent 32%);
}
.footer-content {
position: relative;
z-index: 1;
display: flex;
align-items: flex-end;
justify-content: space-between;
height: 100%;
padding: 32px clamp(24px, 7vw, 120px);
}
.footer-content h2 {
margin: 0 0 8px;
font-size: clamp(24px, 4vw, 44px);
letter-spacing: -.04em;
}
.footer-content p {
max-width: 440px;
margin: 0;
color: rgb(247 249 255 / 68%);
line-height: 1.6;
}
.gradient-guide {
display: flex;
align-items: center;
gap: 12px;
color: rgb(247 249 255 / 68%);
font-size: 12px;
white-space: nowrap;
}
.gradient-guide::before {
display: block;
width: 110px;
height: 8px;
border-radius: 999px;
background: linear-gradient(to right, rgb(255 255 255 / 12%), rgb(255 255 255 / 88%));
content: "";
}
@media (max-width: 720px) {
.demo-page {
min-height: 1500px;
padding-top: 48px;
}
.background-content {
grid-template-columns: 1fr;
margin-top: 100px;
}
.background-card {
min-height: 150px;
}
.background-card strong {
margin-bottom: 24px;
}
.frosted-footer {
height: 240px;
}
.footer-content {
display: block;
padding-top: 96px;
}
.gradient-guide {
margin-top: 22px;
}
}
</style>
</head>
<body>
<main class="demo-page">
<header class="demo-header">
<p class="demo-kicker">BACKDROP FILTER / MASK</p>
<h1>清晰的背景,逐渐变模糊。</h1>
<p>
向下滚动页面,观察底部固定区域:越靠近顶部,背后的网格、文字和卡片越清晰;
越靠近底部,模糊层越多,背景细节越柔和。
</p>
</header>
<section class="background-content" aria-label="用于观察模糊效果的背景内容">
<article class="background-card">
<strong>01</strong>
<span>高对比度文字与网格能够清楚显示 backdrop-filter 的模糊变化。</span>
</article>
<article class="background-card">
<strong>02</strong>
<span>每一层使用不同的 blur 半径,再由 mask 控制它的显示范围。</span>
</article>
<article class="background-card">
<strong>03</strong>
<span>相邻蒙版的透明过渡区域重叠,避免出现突兀的分界线。</span>
</article>
</section>
<div class="background-line" aria-hidden="true"></div>
<aside class="frosted-footer" aria-label="渐变毛玻璃示例">
<div class="frosted-layer frosted-layer-1" aria-hidden="true"></div>
<div class="frosted-layer frosted-layer-2" aria-hidden="true"></div>
<div class="frosted-layer frosted-layer-3" aria-hidden="true"></div>
<div class="frosted-layer frosted-layer-4" aria-hidden="true"></div>
<div class="footer-content">
<div>
<h2>渐变毛玻璃</h2>
<p>1px / 3px / 5px / 7px 四层模糊,沿底部方向逐步增强。</p>
</div>
<div class="gradient-guide">清晰 → 模糊</div>
</div>
</aside>
</main>
</body>
</html>
如果想让下方更模糊、上方更清晰,可以把层配置整体反过来,或者把 to bottom 改成 to top。关键是让更大的模糊半径只出现在需要增强模糊的区域。
五、为什么推荐多层,而不是一层渐变背景
有些实现会写成这样:
css
.glass {
background: linear-gradient(
to bottom,
rgb(255 255 255 / 4%),
rgb(255 255 255 / 70%)
);
backdrop-filter: blur(12px);
}
这段代码可以产生"颜色渐变",但不能产生"模糊强度渐变"。整个元素仍然使用同一个 blur(12px),变化的只是白色覆盖层的透明度。
颜色渐变有时已经够用,但它和真正的渐变毛玻璃有明显区别:
| 实现方式 | 变化的是什么 | 背景细节的模糊程度 |
|---|---|---|
| 渐变背景 + 单层 blur | 覆盖色的透明度 | 始终相同 |
| 多层 blur + mask | 每层模糊结果的可见范围 | 沿方向变化 |
如果需求只是让区域下方颜色更白,第一种写法更简单;如果需要让背景细节也从清晰逐渐变模糊,就需要多层 backdrop-filter。
六、mask-image 和 backdrop-filter 容易踩的坑
1. mask 不是背景色
css
.layer {
mask-image: linear-gradient(to bottom, #000, transparent);
}
这里的黑色不会显示在页面上。mask 做的是透明度裁剪:黑色区域显示元素,透明区域隐藏元素,灰色区域半透明显示元素。
因此,单独给一个空元素加 mask-image,通常看不到任何东西。需要先确认这个元素本身确实有可见内容,例如 backdrop-filter 或半透明背景。
2. mask-image 和 mask 的关系
mask 是一个 shorthand,除了图像外,还可以统一描述遮罩的位置、大小、重复方式等属性。简单场景可以写 mask-image,但在需要兼容 Safari 或维护多个相关属性时,推荐成对写:
css
.layer {
-webkit-mask: var(--mask);
mask: var(--mask);
}
这里的 -webkit-mask 不是为了替代标准属性,而是为了覆盖部分 WebKit 内核环境。两者最好使用同一份变量,避免前缀版本和标准版本的渐变参数不一致。
3. 不要把 mask-composite 当成渐变必需品
mask-composite 用来规定多个蒙版之间如何合成,例如相加、相减或相交。
本文的渐变毛玻璃只需要每个模糊层使用一个线性渐变蒙版,因此不需要 mask-composite。只有在做镂空、边框蒙版或多个遮罩相减时,才需要引入它。
不必要地加入 mask-composite 会增加浏览器差异,尤其是不同内核对组合关键字和前缀的支持并不完全一致。
4. backdrop-filter 处理的是背后的内容
backdrop-filter 处理的是元素背后的区域,不是当前元素中的文字和子元素。
因此,下面的元素需要是半透明的:
css
.glass {
background: rgb(255 255 255 / 12%);
backdrop-filter: blur(12px);
}
如果写成 background: #fff,背景内容会被不透明白色盖住,模糊效果就很难观察到。调试时可以先使用非常低的透明度,例如 rgb(255 255 255 / 8%)。
5. 圆角和溢出裁剪要放在外层
渐变模糊层一般会使用绝对定位铺满容器:
css
.frosted-panel {
position: relative;
overflow: hidden;
border-radius: 28px;
}
.frosted-layer {
position: absolute;
inset: 0;
}
外层的 overflow: hidden 负责裁剪模糊层,border-radius 负责让内部效果跟随容器圆角。否则模糊可能超出卡片边界,四角出现不规则的矩形区域。
如果页面中还有其他定位层,建议让玻璃容器建立独立层叠上下文:
css
.frosted-panel {
isolation: isolate;
}
isolation 可以减少玻璃装饰层和外部元素发生意外混合的问题,但它不能替代对 z-index 和 DOM 层级的检查。
七、如何调节渐变效果
渐变毛玻璃主要调四类参数。
模糊半径
css
--blur: 1px;
--blur: 3px;
--blur: 7px;
模糊半径差距太小,视觉上不容易看出渐变;差距太大,又容易出现层次断裂。通常可以从 1px / 3px / 5px / 7px 这类小步进开始,再根据背景对比度调整。
实色区位置
css
linear-gradient(to bottom, #000 0 60%, transparent 100%);
60% 之前是完全显示区域。这个值越大,该层影响的范围越大。
透明过渡区
css
linear-gradient(to bottom, #000 0 60%, transparent 85%);
从 60% 到 85% 是过渡区。过渡区太短会显得生硬,太长则会让多层效果变得过于平均。相邻层的过渡区应该有一定重叠。
渐变方向
css
to bottom
to top
底部固定栏通常会让模糊从内容区域向底部增强;顶部导航则可能需要反方向。不要直接照搬停止点,要根据组件所在的位置决定方向。
八、性能和兼容性
1. 模糊层越多,成本越高
多层方案的代价是需要多次处理背景。四层可以提供比较平滑的过渡,但并不意味着所有场景都应该固定使用四层。
如果容器面积大、页面滚动频繁,或者运行在性能较弱的移动 WebView 中,可以降为一层:
css
.frosted-panel--simple {
background: rgb(255 255 255 / 22%);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.frosted-panel--simple .frosted-layer {
display: none;
}
2. 提供不支持时的背景
不支持 backdrop-filter 的浏览器至少应该有一个可读的背景:
css
.frosted-panel {
background: rgb(238 244 255 / 92%);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.frosted-panel {
background: rgb(255 255 255 / 8%);
}
}
这样即使模糊没有生效,文字和按钮也不会直接落在复杂背景上。
3. 不要只用桌面浏览器截图判断性能
静态截图只能说明效果是否大致正确,不能说明滚动时是否流畅。实际验证时至少要关注:
- 固定玻璃层是否覆盖了过大的区域;
- 滚动时是否出现明显掉帧;
- Safari、移动浏览器和目标 WebView 是否都能看到模糊;
- 不支持
backdrop-filter时文字是否仍然清晰。
九、排查顺序
如果渐变毛玻璃没有效果,可以按这个顺序检查:
- 玻璃层后面是否有明显的彩色或高对比度内容;
- 玻璃层是否使用了半透明背景,而不是完全不透明的背景;
- 是否同时写了
backdrop-filter和-webkit-backdrop-filter; - 是否同时写了
mask和-webkit-mask; - 临时删除蒙版后,单层模糊是否可以正常显示;
- 外层是否设置了正确的尺寸、圆角和
overflow: hidden; - 各层的蒙版过渡区是否有重叠;
- 最后再检查
z-index、层叠上下文和浏览器支持情况。
调试时不要一开始同时修改模糊半径、透明度、蒙版方向和层级。先验证单层 backdrop-filter,再加 mask,最后增加其他模糊层,问题会更容易定位。
十、总结
渐变毛玻璃的重点,不是找到一个"神奇的 CSS 属性",而是把一个模糊效果拆成多个强度不同的层:
backdrop-filter负责把背景内容模糊;mask负责控制每一层在哪些区域可见;- 多层不同的
blur半径负责形成强度差异; - 重叠的渐变蒙版负责让层与层之间平滑接力;
- 半透明背景、圆角和降级背景负责保证最终可用性。
如果只是需要一个普通的半透明卡片,一层 backdrop-filter 就足够了;如果需要让固定区域和页面内容自然过渡,再考虑多层模糊与蒙版。
最实用的经验是:先用两层或三层做出方向正确的效果,再根据背景复杂度调整模糊半径和蒙版停止点。层数越多不一定越好,真正影响观感的是层之间是否连续,以及页面在真实设备上是否仍然流畅。