Skip to content

纯 css 滚动阴影实现 ​

结论:阴影“永远存在”,只是被遮住/露出 ​

滚动并不会“触发”阴影;阴影一直画在那儿,只是被遮罩盖住。等内容滚动时,遮罩相对阴影移开,阴影就自然露出来了。

核心原理:background-attachment 决定“谁跟着滚” ​

只要记住 local 和 scroll 这两种行为:

值行为
local背景跟着容器内容一起滚动
scroll背景固定在容器本身,不随内容滚动

顶部阴影:遮罩层(local)+ 阴影层(scroll) ​

顶部阴影的核心就是“拼两层 background”:

  1. 阴影层(固定不动):linear-gradient(rgba(0,0,0,.5), transparent) + background-attachment: scroll
  2. 遮罩层(随内容滚动):linear-gradient(#fff, #fff) + background-attachment: local

初始时遮罩盖住阴影,所以看起来“没有阴影”;滚动时遮罩相对移开,阴影自然就露出来了。

关键现象:为什么看起来“只在滚动时出现” ​

  • 未滚动:local 遮罩盖住 scroll 阴影
  • 滚动中:local 相对移动离开阴影区域,scroll 阴影露出来
  • 滚到边界:遮罩又回到阴影区域,阴影随之消失/反向变化

直觉理解:遮罩随滚动,阴影固定 ​

把它想成两层:阴影(scroll)固定在容器上,遮罩(local)跟着内容滚动;遮罩离开后,阴影就“露出来”了。

为什么不用 JS 也能模拟“滚动状态” ​

不需要监听滚动事件。状态已经被编码在 CSS 的“背景相对位移”里:scroll 层固定不动,local 层跟着内容滚动,遮罩移开就露出阴影。

参数怎么调(快速) ​

你真正要关心的只有这些(对应你后面 CSS 里的变量):

  • 阴影:用 linear-gradient 做“深 -> 透明”(不走 box-shadow)
  • 厚度:--thickness(对应 background-size: 100% var(--thickness))
  • 强度/颜色:--shadow-color + --mask-color
  • background-repeat: no-repeat:避免重复绘制

一句话总结(工程师视角) ​

遮罩层(local)和阴影层(scroll)的相对位移,制造出“只在滚动时出现”的视觉效果。

方案速览:顶部 / 底部 / 上下四边分别是多少层 ​

你可以把它理解成“遮罩层(local)+ 阴影层(scroll)”的拼装:

  • 只做顶部(2 层):顶部遮罩 local + 顶部阴影 scroll
  • 只做底部(2 层):底部遮罩 local + 底部阴影 scroll
  • 顶部 + 底部(4 层,本文实现):上遮罩 local、下遮罩 local、上阴影 scroll、下阴影 scroll

工程实现:顶部 + 底部阴影(4 层 background,零 JS) ​

这是推荐的方案:顶部 + 底部一起做(4 层 background)。核心仍然是 local(遮罩跟随内容滚)和 scroll(阴影固定在容器上)的相对位移。

把下面 CSS 加到样式表里,然后给需要的元素加 class="scroll-shadow scroll-shadow-both"(并确保它有 height/maxHeight 让内部可以滚动)。

css
.scroll-shadow {
  /* 让它成为内部滚动容器 */
  overflow: auto;
  padding: 16px;
  box-sizing: border-box;

  /* 参数:可按需覆盖 */
  --thickness: 16px;
  --mask-color: #fff;
  --shadow-color: rgba(0, 0, 0, 0.2);

  background-repeat: no-repeat;
  background-color: var(--mask-color);
}

.scroll-shadow-both {
  background-size:
    100% var(--thickness),
    100% var(--thickness),
    100% var(--thickness),
    100% var(--thickness);

  background-position:
    top left,
    bottom left,
    top left,
    bottom left;

  background-attachment:
    local,
    local,
    scroll,
    scroll;

  background-image:
    linear-gradient(var(--mask-color), var(--mask-color)),
    linear-gradient(var(--mask-color), var(--mask-color)),
    linear-gradient(var(--shadow-color), transparent),
    linear-gradient(transparent, var(--shadow-color));
}

重要前提(否则看不到效果):

  • 容器内部需要滚动:overflow: auto + height/maxHeight
  • --mask-color 最好与容器真实背景一致

React 封装:ScrollShadow(零 JS) ​

在 React 里你要做的事情非常少:把参数交给 CSS 变量,其余背景层逻辑仍然由 CSS 完成。

下面给一个简单可用的封装(direction 支持 top/bottom/both,如果你只需要单边也更省层数)。

组件代码(TypeScript) ​

tsx
import React from "react";

type Direction = "top" | "bottom" | "both";

export type ScrollShadowProps = {
  children: React.ReactNode;
  className?: string;
  style?: React.CSSProperties; // 可以传 height/maxHeight 等

  direction?: Direction;
  thickness?: number; // px
  shadowOpacity?: number; // 0~1
  maskColor?: string; // 建议和容器背景一致
};

export function ScrollShadow({
  children,
  className = "",
  style,
  direction = "both",
  thickness = 16,
  shadowOpacity = 0.2,
  maskColor = "#ffffff",
}: ScrollShadowProps) {
  const vars = {
    ["--thickness" as any]: `${thickness}px`,
    ["--mask-color" as any]: maskColor,
    ["--shadow-color" as any]: `rgba(0,0,0,${shadowOpacity})`,
  } as React.CSSProperties;

  const dirClass =
    direction === "top"
      ? "scroll-shadow-top"
      : direction === "bottom"
        ? "scroll-shadow-bottom"
        : "scroll-shadow-both";

  return (
    <div
      className={["scroll-shadow", dirClass, className].filter(Boolean).join(" ")}
      style={{ ...vars, ...style }}
    >
      {children}
    </div>
  );
}

对应 CSS(3 个类:通用容器 + 上/下/both) ​

css
.scroll-shadow {
  overflow: auto;
  padding: 16px;
  box-sizing: border-box;
  background-repeat: no-repeat;
  background-color: var(--mask-color);
}

.scroll-shadow-both {
  background-size:
    100% var(--thickness),
    100% var(--thickness),
    100% var(--thickness),
    100% var(--thickness);
  background-position:
    top left,
    bottom left,
    top left,
    bottom left;
  background-attachment:
    local,
    local,
    scroll,
    scroll;
  background-image:
    linear-gradient(var(--mask-color), var(--mask-color)),
    linear-gradient(var(--mask-color), var(--mask-color)),
    linear-gradient(var(--shadow-color), transparent),
    linear-gradient(transparent, var(--shadow-color));
}

.scroll-shadow-top {
  background-size:
    100% var(--thickness),
    100% var(--thickness);
  background-position:
    top left,
    top left;
  background-attachment:
    local,
    scroll;
  background-image:
    linear-gradient(var(--mask-color), var(--mask-color)),
    linear-gradient(var(--shadow-color), transparent);
}

.scroll-shadow-bottom {
  background-size:
    100% var(--thickness),
    100% var(--thickness);
  background-position:
    bottom left,
    bottom left;
  background-attachment:
    local,
    scroll;
  background-image:
    linear-gradient(var(--mask-color), var(--mask-color)),
    linear-gradient(transparent, var(--shadow-color));
}

使用示例 ​

tsx
<ScrollShadow direction="both" style={{ height: 200 }}>
  {/* 列表内容 */}
</ScrollShadow>

常见坑与验证 ​

  1. --mask-color 一定要尽量贴近容器真实背景色,否则遮罩“颜色不一致”会很明显
  2. 容器必须能滚动(必须有 overflow: auto,同时给出 height/maxHeight),否则不会触发相对位移
  3. 调参顺序建议是:先把 --thickness 调到你想要的阴影厚度,再调 shadowOpacity 强度
  4. 快速验证:把滚动条从“顶部”滚到“中间/底部”,看阴影是否按预期“出现 -> 消失(或反向)”