Skip to content
在聊天面板、评论区、feed 流这类容器中,常需让滚动到边缘的内容产生渐隐效果。mask-image 能够在不改变布局的前提下完成这一处理,但其在绘制阶段会引入额外的纹理与合成开销。本节从该属性的渲染管线位置开始,逐步说明用法、性能边界和兼容性情况。
基本概念
mask-image 是 CSS Masking Module Level 1 定义的属性,用于通过一张图像(或渐变)控制元素的可见区域。默认的 mask-mode 为 alpha,即使用掩码图像的 alpha 通道决定对应区域的透明度:alpha 为 0 处完全透明,alpha 为 1 处完全显示。
工作原理
mask-image 的掩码生成和合成发生在 Paint 阶段内部,并非独立的管线阶段。整体流程可以表述为:
Layout → Paint(包含掩码纹理创建和遮罩合成)→ Composite → 屏幕
在这个位置下会产生几点影响:
- 不影响 Layout:掩码不改变元素的盒模型尺寸或位置,布局计算不受任何影响。
- 不影响 DOM 事件:被掩码变为透明的区域仍然处于正常文档流中,点击、触摸等事件照常触发。如果确实需要移除交互能力,应额外设置
pointer-events: none。 - 纹理生成:浏览器在绘制时,会根据掩码图像生成一张尺寸与元素绘制区域对应的纹理,其 alpha 通道与元素绘制结果相乘。使用
linear-gradient等渐变作为掩码时,该纹理会在每次绘制时重新创建。纹理的实际像素尺寸为CSS 像素 × 设备像素比,因此在高分辨率屏幕下纹理内存占用会成倍增加。
基本用法
基础渐变
让一个滚动容器在顶部 3vh 和底部 20vh 处将内容渐隐至透明:
css
.scroll-container {
overflow: auto;
mask-image: linear-gradient(
to bottom,
transparent 0%,
transparent 3vh,
black 10vh,
black calc(100% - 20vh),
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
transparent 3vh,
black 10vh,
black calc(100% - 20vh),
transparent 100%
);
}渐变中的 transparent 对应 alpha=0,black 对应 alpha=1。0% 到 3vh 之间 alpha 保持为 0(完全透明),3vh 到 10vh 之间 alpha 从 0 线性过渡到 1,10vh 到 calc(100%-20vh) 保持完全显示,底部最后 20vh 再从 1 过渡回 0。最终浏览器会将这个渐变光栅化为一张与容器绘制区域等大的纹理,在 Paint 阶段与容器内容合成。
如果不希望顶部和底部边缘出现太锐利的切换,可以适当拉长过渡区间;反之,如果需要更硬的截断,则缩小颜色停止点之间的距离。
颜色与事件处理
black 还是 white?
默认 mask-mode: alpha 下,颜色的 alpha 通道决定透明度,因此 black(alpha=1)与 white(alpha=1)的效果完全一致。若显式切换为 mask-mode: luminance(Firefox 支持),亮度越高的区域越不透明,此时用 white 表示可见区域语义更明确。但 luminance 模式并非默认值且极少使用,一般保留 black 即可。
渐隐区域使用 vh 还是 px?
vh 使渐隐区域比例与视口高度关联,移动端聊天列表常用 5–10vh。固定高度容器或需要精确控制距离时,px 更适用。
透明区域仍可响应事件
即使内容被掩码隐藏,其上的点击事件依然会触发。当容器内存在按钮或链接时,用户可能在“看不见的区域”产生误触。可以通过内边距将内容区收拢,使交互热点与可见区对齐,无需移除掩码:
css
.scroll-container {
padding-top: 3vh;
padding-bottom: 20vh;
mask-image: /* 同一个渐变 */;
}API
mask-image 是 mask 简写属性的一个子属性。简写属性可以一次性设置多个与遮罩相关的值,但在 Safari 中的稳定性不如长写形式,实际使用时更推荐直接书写 mask-image / -webkit-mask-image。
常与 mask-image 配合使用的子属性:
mask-mode:默认match-source,对于<image>类型实际表现为alpha。可以显式设置为alpha或luminance。mask-repeat:当使用<image>并设置mask-size时控制重复方式。渐变默认会拉伸至整个元素边框盒,不需要此属性。mask-size:控制掩码图像尺寸。默认为auto,对渐变则拉伸填充。mask-position:控制掩码图像起始位置,默认center。
mask 简写示例(不推荐):
css
mask: linear-gradient(to bottom, transparent, black) center / cover no-repeat;
-webkit-mask: linear-gradient(to bottom, transparent, black) center / cover no-repeat;Firefox 仅识别无前缀的 mask,Chrome 120 之前仅识别 -webkit-mask,Safari 两者皆可。直接使用 mask-image / -webkit-mask-image 可以避免这些差异带来的不确定行为。
示例
示例一:完整布局下的边缘渐隐
html
<div class="chat-panel">
<ul class="message-list">
<li class="message">...</li>
<!-- 多条消息 -->
</ul>
</div>css
.chat-panel {
height: 60vh;
background: #f5f5f5;
}
.message-list {
height: 100%;
overflow-y: auto;
padding: 2vh 1rem;
mask-image: linear-gradient(
to bottom,
transparent,
transparent 2vh,
black 6vh,
black calc(100% - 10vh),
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent,
transparent 2vh,
black 6vh,
black calc(100% - 10vh),
transparent 100%
);
}在此例中,顶部 2vh 完全透明,2vh 到 6vh 逐渐显现,底部 10vh 开始渐隐。若列表内容不足一屏高,则可能看不到底部渐隐效果,但掩码本身仍会参与合成。
示例二:纯色背景下的伪元素遮罩(替代方案)
当容器背景为纯色时,可以用 sticky 定位的伪元素叠加渐变背景,避免生成 mask 纹理:
css
.pure-bg-container {
position: relative;
background: #fff; /* 纯色 */
overflow: auto;
}
.pure-bg-container::before,
.pure-bg-container::after {
content: '';
position: sticky;
display: block;
height: 5vh;
background: linear-gradient(to bottom, #fff, transparent);
pointer-events: none;
}
.pure-bg-container::before {
top: 0;
}
.pure-bg-container::after {
bottom: 0;
background: linear-gradient(to top, #fff, transparent);
}伪元素在 Paint 阶段绘制自身的渐变背景,合成阶段与内容层叠,整个过程不产生额外的 mask 纹理。pointer-events: none 确保它们不会拦截操作。
局限在于:如果 --bg-color 不是纯色(如图案、图片、动态背景),伪元素渐变的固定颜色就会“穿帮”。此时只能回退到 mask-image。
示例三:动态启用遮罩以降低滚动中的重绘开销
对于内容频繁滚动且纹理生成开销明显的场景,可以通过 JavaScript 只在滚动停止后应用遮罩。滚动过程中移除遮罩,避免持续生成纹理。
js
const container = document.querySelector('.scroll-container');
let scrollEndTimer;
container.addEventListener('scroll', () => {
// 滚动中移除遮罩类
container.classList.remove('mask-applied');
clearTimeout(scrollEndTimer);
scrollEndTimer = setTimeout(() => {
// 滚动停止 150ms 后重新应用
container.classList.add('mask-applied');
}, 150);
});css
.scroll-container.mask-applied {
mask-image: linear-gradient(/* ... */);
-webkit-mask-image: linear-gradient(/* ... */);
}这样做在快速滚动时不会有渐隐效果,但能大幅降低 Paint 阶段的纹理创建压力。当用户停止滚动后,遮罩恢复,内容边缘出现渐隐。
示例四:使用 DevTools 观察渲染开销
可以在浏览器开发者工具中确认 mask-image 引发的重绘区域:
- Chrome DevTools → 更多工具 → 渲染(Rendering)→ 勾选“绘制闪烁(Paint flashing)”。
- 滚动目标容器,出现绿色高亮的区域即为发生重绘的范围。若使用
mask-image,整个容器区域通常在每次滚动时都会闪烁,说明纹理被反复创建。 - 通过 Performance 面板录制滚动操作,可在“帧(Frames)”区域看到明显高于 16ms 的 Paint 耗时,点击具体帧还能查看 Paint 调用的细节。
通过以上手段可以定量评估是否需要用伪元素替代或动态掩码策略。
注意点
- 透明区域虽不可见,但仍然参与可访问性树(accessibility tree),若其中包含大量可聚焦元素,可能给屏幕阅读器用户带来困惑。需要时可用
visibility: hidden或移除节点。 - 渐变中的颜色停止点应仔细设置,中间突然跳变可能造成锯齿状边缘,建议使用平滑过渡。
mask-image不支持 IE 11,且部分浏览器需前缀,具体兼容性见下文表格。- 使用
mask简写时,Safari 对多个值的解析存在差异,更推荐长写形式。
浏览器兼容性
| 浏览器 | mask-image | -webkit-mask-image |
|---|---|---|
| Chrome 58–119 | ❌ | ✅ |
| Chrome 120+ | ✅ | ✅(别名) |
| Firefox 53+ | ✅ | ❌ |
| Safari 15.4+ | ✅ | ✅ |
| Safari < 15.4 | ❌ | ✅ |
| IE 11 | ❌ | ❌ |
同时书写标准属性与 -webkit-mask-image 可覆盖所有现代浏览器。Chrome 120 之前仅识别前缀属性,Firefox 仅识别标准属性,Safari 在两套写法下均可工作。若项目需兼容旧版 Safari(< 15.4),只写标准属性会导致无渐隐效果,但内容仍正常显示,属于非破坏性降级。
限制
纹理生成与重绘开销
掩码纹理在每次 Paint 时创建。内容持续变化(快速滚动、动画播放)时,每一帧都需要重新生成纹理。对于包含大量条目的列表,高帧率下纹理的频繁绘制可能拉低帧率。
纹理内存占用
纹理尺寸为元素绘制区域乘以设备像素比。一个 400×800px 的容器在 1x 屏幕上,对应 RGBA 纹理约占 1.3 MB。2x 屏幕下为 800×1600px,约 5 MB;3x 屏幕更大。如果页面中有多个使用 mask-image 的容器,纹理内存总量需纳入考量,尤其在移动端。
合成层交叉
当容器内同时使用 backdrop-filter、will-change: transform 等触发独立合成层的属性时,掩码纹理需要与这些合成层进行交叉合成,合成线程的计算复杂度随之上升。
伪元素替代的局限
伪元素遮罩依赖背景色实现“颜色渐隐”——从背景色过渡到透明。如果容器背景是复杂图案、图片或动态效果,渐隐区域会暴露出伪元素的固定底色,效果失效。此时无通用且轻量的替代手段,只能回退到 mask-image。即使开销较高,仍可通过以下方式降低影响:
- 为容器设置
will-change: transform或transform: translateZ(0),将掩码提升至独立合成层,减少重复纹理生成的影响范围。 - 限制掩码更新频率,例如在滚动停止后应用掩码(示例三)。
- 避免在掩码容器内同时触发额外的合成层,控制交叉复杂度。
应用
需要内容边缘渐隐时,可根据容器背景类型和内容更新频率选择方案:
- 纯色背景且内容高频滚动(如消息列表),优先用伪元素遮罩,无 mask 纹理开销。
- 背景为复杂图案或需要精确遮盖非矩形区域,使用
mask-image。 - 静态或低频更新内容(如评论区),使用
mask-image的开销通常可接受。
