Appearance
CSS 内容边缘渐隐
让滚动内容在到达容器边缘时逐渐透明消失——这个效果在聊天、评论区、feed 流中很常见。核心工具是 mask-image。
mask-image 的渲染管线位置
mask-image 在浏览器渲染管线中属于 Paint 阶段之后、Composite 之前。具体来说:
text
Layout → Paint → Apply Mask → Composite → 屏幕这意味着:
- 不影响 Layout:mask 不会改变元素的盒模型大小或位置,布局计算完全不受影响
- 不影响 DOM 事件:被 mask 隐藏的区域仍然接收鼠标事件。如果需要完全移除交互,需要配合
pointer-events: none - GPU 纹理开销:mask 本质上是一个额外的纹理层。
mask-image用一个 alpha 通道纹理与元素的绘制结果相乘。在使用linear-gradient作为 mask 时,浏览器需要为 mask 生成一个与元素尺寸相同的 GPU 纹理
生产级实现
css
.scroll-container {
overflow: auto;
/* mask 在上下边缘产生渐隐,中间区域完全可见 */
mask-image: linear-gradient(
to bottom,
transparent 0%,
transparent 3vh, /* 顶部 3vh 内从完全透明过渡 */
black 10vh, /* 10vh 处完全可见 */
black calc(100% - 20vh), /* 底部 20vh 前保持可见 */
transparent 100% /* 底部边缘渐隐至透明 */
);
/* 标准写法 + WebKit 前缀保证 Safari 兼容 */
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
transparent 3vh,
black 10vh,
black calc(100% - 20vh),
transparent 100%
);
}几个工程决策点:
为什么用 black 而不是 rgba(255,255,255,1)? mask-image 使用的是图片的 alpha 通道(或亮度通道,取决于 mask-mode)。在默认的 mask-mode: alpha 下,颜色无关紧要——black(rgba(0,0,0,1))和 white(rgba(255,255,255,1))的 alpha 值都是 1,效果完全相同。区别在于 mask-mode: luminance 时(Firefox 支持),亮度越高越不透明——此时用 white 更直观。但 mask-mode: luminance 不是默认值,所以对大多数场景 black 和 white 等价。
渐隐区域用 vh 还是固定 px? vh 保证在不同屏幕尺寸下渐隐区域的比例一致。对于手机端聊天列表,渐隐区域 5-10vh 是一个合适的范围。px 适合固定高度容器。
transparent 区域的事件问题。 被 mask 变透明的区域仍然在 DOM 中、仍然接收事件。如果这是可滚动的聊天列表,用户可能在"看不见的区域"误触。解决:
css
.scroll-container {
mask-image: ...;
/* 通过 padding 将内容限制在可见区域内,避免不可见区域误触 */
padding-top: 3vh;
padding-bottom: 20vh;
/* 配合 mask 让 padding 区域的内容不可见 */
}性能:mask 在大量内容下的退化
mask 不是免费的效果。在以下场景中性能开销会变得可测量:
1. 频繁重绘。 mask 是 Paint 阶段的产物。如果元素内容频繁变化(如列表滚动、动画),每次重绘都需要重新生成 mask 纹理。对于包含数百条消息的聊天列表,在快速滚动时 mask 纹理的重复生成会导致帧率下降。
2. GPU 纹理内存。 mask 生成的纹理大小等于元素的布局尺寸(width × height)。一个 400×800px 的容器,mask 纹理约 1.3MB(RGBA,32位)。对于 5 个同时可见的渐隐容器,GPU 纹理占用约 6.5MB——在移动端这不是可以忽略的数字。
3. 合成层爆炸。 如果容器内还有 backdrop-filter、will-change: transform 等其他触发独立合成层的属性,mask 纹理会与这些层产生交叉引用,增加合成线程的复杂度。
优化策略:
css
/* ✅ 对静态内容——mask 开销可忽略 */
.static-content {
mask-image: linear-gradient(...);
}
/* ⚠️ 对高频滚动内容——考虑用伪元素替代 */
.scroll-container::before,
.scroll-container::after {
content: '';
position: sticky;
display: block;
height: 5vh;
background: linear-gradient(to bottom, var(--bg-color), transparent);
pointer-events: none;
}
/* 伪元素遮罩方案不生成 mask 纹理,仅在 Composite 阶段叠加 */伪元素方案的 tradeoff:它只能做"颜色渐隐"(从背景色渐变到透明),不能做真正的"内容透明度"调整。如果容器背景是复杂图案或图片,伪元素方案失效,必须用 mask。
兼容性
| 浏览器 | mask-image | -webkit-mask-image |
|---|---|---|
| Chrome 58+ | ✅ | ✅(别名) |
| Firefox 53+ | ✅ | ❌ |
| Safari 15.4+ | ✅ | ✅(早期版本唯一支持) |
| Safari < 15.4 | ❌ | ✅ |
| IE 11 | ❌ | ❌ |
在生产代码中,同时写标准属性和 -webkit- 前缀能覆盖所有现代浏览器。Safari 在 15.4 之前只支持 -webkit-mask-image,所以如果项目需要兼容旧 Safari,仅写标准属性会导致渐隐失效(内容正常显示,无渐隐效果——非破坏性降级)。
CSS 标准中 mask 是一个简写属性(类似 background),可以一次性设置多个 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;但简写属性在 Safari 中的兼容性不如长写属性稳定,因此这里更适合使用 mask-image / -webkit-mask-image 的长写形式。
