写 CSS 阴影最容易翻车的不是参数,是”系统”。一个项目里同时混用 Material 风和 iOS 风,立刻显得业余。先选一套,再调参数,是出活的顺序。
单层阴影为什么像贴纸
.card { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25); }
这是大多数人第一次写的样子,看上去就是”PPT 阴影”。问题:
- 不透明度 25% 太高,物理世界里阴影边缘是从透明到半透明渐变,不是均匀深色
- 没有环境遮蔽,元素和背景之间缺一层贴近、低对比的暗化
- 方向单一,仿佛被一束 1° 直径的激光从正上方照射
修法是多层叠加:
.card {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.06), /* ambient: 紧贴元素的暗化 */
0 4px 8px rgba(0, 0, 0, 0.08), /* mid: 中等距离的方向影 */
0 16px 24px rgba(0, 0, 0, 0.06); /* far: 远距离柔和影 */
}
三层加起来不到 20% 总暗度,但层次远比 25% 单层好看。
Material elevation 系统
Material Design 把阴影量化成 dp(density-independent pixel)的”高度”,每个高度对应两层投影:一层 ambient(环境光遮蔽),一层 key light(来自上方 45°)。
常用档位(Material 3 简化版):
:root {
--elevation-1: 0 1px 2px rgba(0,0,0,.06), 0 1px 3px rgba(0,0,0,.10);
--elevation-2: 0 1px 2px rgba(0,0,0,.06), 0 2px 6px rgba(0,0,0,.12);
--elevation-3: 0 1px 3px rgba(0,0,0,.08), 0 4px 12px rgba(0,0,0,.14);
--elevation-4: 0 2px 4px rgba(0,0,0,.10), 0 8px 24px rgba(0,0,0,.16);
--elevation-5: 0 4px 8px rgba(0,0,0,.10), 0 16px 32px rgba(0,0,0,.20);
}
.card { box-shadow: var(--elevation-1); }
.card:hover { box-shadow: var(--elevation-3); }
.modal { box-shadow: var(--elevation-5); }
层级靠高度差表达:卡片是 1,按钮悬停升到 3,弹窗是 5。同一页面里超过 3 个不同高度就会乱,控制在 2-3 档。
iOS / Apple HIG 风格
iOS 不用 elevation。它的层级靠:
- 半透明 + 模糊:
backdrop-filter: blur(20px); background: rgba(255,255,255,0.7); - 单层柔和阴影:仅在卡片”飘起来”时用一次
- 细边框:
border: 0.5px solid rgba(0, 0, 0, 0.08);
iOS 风的卡片:
.ios-card {
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(40px) saturate(180%);
border: 0.5px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.06);
border-radius: 16px;
}
阴影只一层、模糊半径很大、不透明度极低。整体感觉”轻”——和 Material 的”重”形成对比。
拟物 / Neumorphism
2020 年流行过的”软 UI”靠 inset shadow + 反向高光做凹凸:
.neuro-button {
background: #e0e5ec;
border-radius: 16px;
box-shadow:
-6px -6px 12px rgba(255, 255, 255, 0.9), /* 左上高光 */
6px 6px 12px rgba(163, 177, 198, 0.6); /* 右下暗影 */
}
.neuro-button:active {
box-shadow:
inset -4px -4px 8px rgba(255, 255, 255, 0.9),
inset 4px 4px 8px rgba(163, 177, 198, 0.6);
}
只能在接近背景色的元素上用,背景必须是中等灰。对比度差,可访问性差,慎用。
暗色模式策略
黑底加黑阴影 = 几乎看不见。三种应对:
方案 A — 放弃阴影,用边框:
[data-theme="dark"] .card {
box-shadow: none;
border: 1px solid rgba(255, 255, 255, 0.08);
}
方案 B — 用背景色阶:
[data-theme="dark"] {
--bg: #0a0a0a;
--card: #1a1a1a; /* 比背景亮 */
--card-elevated: #242424; /* 比卡片更亮 */
}
方案 C — 加大不透明度 + 加白色 inset 高光:
[data-theme="dark"] .card {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.5),
0 8px 24px rgba(0, 0, 0, 0.4),
inset 0 0 0 1px rgba(255, 255, 255, 0.04); /* 顶边一根高光 */
}
方案 B 最稳,A 配合主题色边框很现代,C 用在不能改背景色的地方(比如卡片要透明)。
text-shadow / drop-shadow / box-shadow
三个名字接近但完全不同:
| 属性 | 作用对象 | 是否跟随 alpha 边缘 | GPU 合成 |
|---|---|---|---|
box-shadow | 元素的 border-box 矩形 | ✗(永远是矩形阴影) | 是 |
text-shadow | 文字本身的字形 | ✓ | 是 |
filter: drop-shadow() | 元素实际像素(含透明) | ✓ | 是 |
带圆角的 PNG 图片用 box-shadow 会出现”矩形阴影”——必须用 filter: drop-shadow():
.cutout-img { filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.2)); }
drop-shadow 不支持 spread,多层阴影只能 drop-shadow() drop-shadow() 串起来,性能比 box-shadow 差。能用 box-shadow 不要用 drop-shadow。
性能
阴影是合成层操作,正常情况零成本。出问题的场景:
- 大模糊 + 大元素 + 滚动:阴影边缘要跟随重绘
- 频繁动画 box-shadow:每帧重算阴影,GPU 也吃不消
修法:
/* 把阴影挪到伪元素,只动 opacity */
.card { position: relative; }
.card::after {
content: '';
position: absolute; inset: 0;
border-radius: inherit;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
opacity: 0;
transition: opacity .2s;
pointer-events: none;
}
.card:hover::after { opacity: 1; }
动画 opacity 永远比动画 shadow 流畅。
一句话总结
先选系统(Material elevation 或 iOS 毛玻璃),不要混;暗色模式优先换层级而不是叠阴影。