Skip to content

CSS 内容边缘渐隐

让滚动内容在到达容器边缘时逐渐透明消失——这个效果在聊天、评论区、feed 流中很常见。核心工具是 mask-image


mask-image 的渲染管线位置

mask-image 在浏览器渲染管线中属于 Paint 阶段之后、Composite 之前。具体来说:

text
Layout → Paint → Apply Mask → Composite → 屏幕

这意味着:

  1. 不影响 Layout:mask 不会改变元素的盒模型大小或位置,布局计算完全不受影响
  2. 不影响 DOM 事件:被 mask 隐藏的区域仍然接收鼠标事件。如果需要完全移除交互,需要配合 pointer-events: none
  3. 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 下,颜色无关紧要——blackrgba(0,0,0,1))和 whitergba(255,255,255,1))的 alpha 值都是 1,效果完全相同。区别在于 mask-mode: luminance 时(Firefox 支持),亮度越高越不透明——此时用 white 更直观。但 mask-mode: luminance 不是默认值,所以对大多数场景 blackwhite 等价。

渐隐区域用 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-filterwill-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 的长写形式。