纯 css 滚动阴影实现
结论:阴影“永远存在”,只是被遮住/露出
滚动并不会“触发”阴影;阴影一直画在那儿,只是被遮罩盖住。等内容滚动时,遮罩相对阴影移开,阴影就自然露出来了。
核心原理:background-attachment 决定“谁跟着滚”
只要记住 local 和 scroll 这两种行为:
| 值 | 行为 |
|---|---|
local | 背景跟着容器内容一起滚动 |
scroll | 背景固定在容器本身,不随内容滚动 |
顶部阴影:遮罩层(local)+ 阴影层(scroll)
顶部阴影的核心就是“拼两层 background”:
- 阴影层(固定不动):
linear-gradient(rgba(0,0,0,.5), transparent)+background-attachment: scroll - 遮罩层(随内容滚动):
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>常见坑与验证
--mask-color一定要尽量贴近容器真实背景色,否则遮罩“颜色不一致”会很明显- 容器必须能滚动(必须有
overflow: auto,同时给出height/maxHeight),否则不会触发相对位移 - 调参顺序建议是:先把
--thickness调到你想要的阴影厚度,再调shadowOpacity强度 - 快速验证:把滚动条从“顶部”滚到“中间/底部”,看阴影是否按预期“出现 -> 消失(或反向)”