说个真实的观察:很多项目里的 box-shadow 永远是同一套参数,做出来不是“浮起来”而是“飘着”,甚至让整个界面透着一股廉价感。阴影这东西,大多数人只用了它的默认形态,却忽略了它背后其实是一套“光源叙事”——CSS 阴影的高级应用,核心不是加多少层参数,而是搞清楚阴影到底在向用户传达什么物理信息。这篇文章想聊的,就是怎么用 box-shadow、drop-shadow、伪元素和动效组合,把界面里的深度、层次、材质和交互反馈真正做出来。
适合谁看?写后台管理系统但总觉得卡片“不够精致”的前端,做营销页需要高质感视觉的页面仔,以及想把自己的组件库阴影抽成设计规范的工程化选手。我不会堆一堆炫技代码,所有方案都以“能落地、好维护、不卡顿”为前提,配合实战踩坑记录,读完就能直接用。
1. 阴影为什么总是“一眼假”:先拆参数,再建光源直觉
1.1 五个参数里,最容易被低估的是颜色和模糊半径
box-shadow 的完整语法是 offset-x offset-y blur-radius spread-radius color,看起来简单,但很多人对参数的理解停留在“水平偏移、垂直偏移、模糊程度”。这没错,但不够。真正决定阴影真实感的,是 blur-radius 和 color 这两个参数。
物理世界里,阴影的“软硬程度”取决于光源面积和物体距离。太阳这种远距离点光源,影子边缘偏硬;室内柔光灯箱这种大面光源,影子边缘过渡柔和。CSS 里 blur-radius 就是控制这个过渡带的宽度。数值越大,边缘虚化越明显,物体看起来就越是“悬浮在柔和环境光里”;数值小,边缘锐利,更适合模拟直射光下的紧凑投影。
颜色同样关键。纯黑阴影在真实环境几乎不存在——光线经过物体边缘绕射和背景反射后,投影会带上环境的色调。日光下偏蓝灰,暖色灯光下偏棕灰。所以阴影颜色别直接写 #000,更通用的做法是用半透明的黑:rgba(0, 0, 0, 0.08)。透明度压低,阴影自然融进背景。这个习惯改掉之后,即便参数不变,观感也会立刻干净很多。
1.2 UI 默认的“左上光源”是怎么来的
图形界面设计里有个默认约定:光源来自左上。这套约定从拟物时代一直延续到现代扁平界面,操作系统的按钮高光、卡片的投影方向,全是这个逻辑。box-shadow: 0 2px 8px rgba(0,0,0,0.1) 这种“底部阴影”其实已经隐含了一个从上方打光、物体略微悬空的物理假设。
有趣的是,很多高级界面会故意打破这个默认。比如底部弹出的 Action Sheet 或导航栏,阴影更多是向上扩散的——因为光从屏幕后方来,或者用户心理上感觉底部元素是“浮在前层”。这不是随便写的,而是因为阴影方向决定了层级暗示。想把一个元素做出“向上浮起”的效果,offset-y 用负值;想让它“沉入背景”,offset-y 趋近 0、模糊半径减小。
我自己的习惯是:全局统一一个“主光源方向”——默认选 0 4px 12px 这类底部为主的参数组,然后通过阴影的透明度渐变来暗示距离,而不是在不同组件里随手改方向。方向一旦混乱,用户虽然说不清哪里不对,但整体就是“不安定”。这就是光源叙事要解决的第一层问题。
1.3 用两层阴影模拟真实环境的双重照明
真实世界里没有任何物体只被一束光照亮。环境里总有来自四面八方的漫反射,加上主光源的方向性投影,共同构成我们看到的效果。CSS 的单层阴影很难模拟这种复合照明,所以实战中普遍会用“双层阴影”甚至“三层阴影”。
比如一个典型的浮层卡片:
css复制.card {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.04),
0 8px 24px rgba(0, 0, 0, 0.08);
}
第一层 0 1px 2px 负责“接触阴影”——物体边缘紧贴背景的细微压痕;第二层 0 8px 24px 负责“环境投影”——物体整体抬升带来的大范围柔影。前者锐利、透明度低、位移小,后者模糊、透明度高、位移大。这两层组合起来,比任何单层大模糊阴影都更接近真实视觉效果。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多层阴影的实战组合:接触阴影、悬浮阴影与环境反射
2.1 接触阴影:让卡片真正“站在”背景上
接触阴影这个词,我最早是从 3D 渲染里听来的。物体和地面接触的位置,因为遮挡了环境光,会形成一个紧密贴合轮廓的暗部。反映到 CSS 里,就是那种位移极小、模糊极小、透明度适中的阴影。很多人觉得卡片“飘”,缺的就是这一层。
具体做法就是上面那个例子里的第一层。但有一个容易忽略的细节:接触阴影的 offset-y 应该和元素的相对位置保持一致。比如鼠标悬停时元素上移 translateY(-4px),接触阴影也要同步上移一点,否则会出现“物体离开地面但影子还留在原地”的穿帮。
我在实际项目里常用一个更精细的版本,把接触阴影做成两层:
css复制.card {
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.02),
0 1px 2px rgba(0, 0, 0, 0.05),
0 12px 32px rgba(0, 0, 0, 0.08);
}
第一层用 0 0 0 1px 加低透明度黑,等于是给卡片描了一道极淡的边,用来抵消浅色背景上阴影落差不足的问题;第二层才是真正的接触阴影。这一套在白色后台界面里尤其管用,卡片和背景的区分瞬间就清晰了。
2.2 悬浮阴影:距离越远,模糊越大、透明度越低
悬浮阴影的规则很简单:元素离背景越远,投影范围越大,边缘越模糊,整体透明度越低。这是因为距离增加后,环境光对影子的“稀释”作用增强,影子不再像近距离那样浓重。这个物理规律直接决定了 hover 动效里阴影该怎么写。
很多开发者的 hover 只是把阴影数值整体放大,比如 0 2px 8px 改成 0 8px 32px。这样做能看出变化,但不够细腻。正确做法是同步调低透明度:
css复制.card {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.05),
0 8px 24px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.25s ease, transform 0.25s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.04),
0 16px 48px rgba(0, 0, 0, 0.12);
}
注意第一层接触阴影的透明度从 0.05 降到了 0.04,位移从 1px 变成 2px——因为物体抬升后,接触阴影其实变淡了,但接触带更宽。第二层悬浮阴影的透明度从 0.08 升到 0.12,同时模糊半径从 24px 变成 48px。这一升一降,才是“抬起”手感的关键。
2.3 三层阴影叠法:一个可以直接抄作业的示例
三层阴影是我在组件库里比较常用的配置,适合做弹窗、下拉菜单、浮出层这类需要明确“我在最上层”的组件。
css复制.popover {
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.03),
0 2px 8px rgba(0, 0, 0, 0.06),
0 24px 64px rgba(0, 0, 0, 0.16);
}
第一层是描边,让浮出层在复杂背景上也有清晰的轮廓;第二层是接触阴影,建立和触发元素之间的空间关系;第三层是大范围柔影,把整个层从页面里“撑”出来。三层各有分工,缺一层都会差点意思。
需要注意:spread-radius 在这里面的作用被很多人误解。它不只是“把阴影放大一圈”,而是控制阴影的填充面积。比如 0 8px 24px -12px,负的 spread 会让阴影在模糊之前先向内收缩,得到更聚拢的投影,适合模拟近距离物体的影子;正的 spread 则适合做“光晕”而不是“阴影”。所以调阴影时,spread 和 blur 是配套使用的,别只盯着 blur 调。
3. box-shadow与drop-shadow的分工:透明素材、SVG、头像与文字发光
3.1 box-shadow的盒模型局限在哪
box-shadow 有一个先天限制:它永远沿着元素的盒模型边缘走。对一个 <div> 来说是四条边,对一个矩形图片来说是矩形轮廓。可一旦元素里有透明区域,比如 PNG 抠图、透明文字、SVG 图标,box-shadow 就会露馅——它照着矩形边缘投射,而不是照着像素轮廓。
举个例子:一张透明背景的小猫 PNG,想给它加阴影,box-shadow 会投出一个矩形影子,小猫耳朵和尾巴的位置完全是空的,看起来就像一块玻璃板底下压了只纸片猫。这个问题我见过很多次,尤其在电商活动页和头像组件里。
3.2 drop-shadow():真正跟随元素轮廓的阴影
filter: drop-shadow() 能解决这个问题。它不是按盒模型算,而是按元素渲染出来的实际像素轮廓计算投影。对透明 PNG、SVG、带 border-radius 的元素、甚至文字都有效。
css复制.avatar {
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.25));
}
头像如果是圆形裁切,box-shadow 也能做到一样的效果;但如果头像本身是 PNG 透明底的不规则形状,box-shadow 就无能为力,drop-shadow 却能精确地沿着头像轮廓投出阴影。这在做用户头像、品牌 Logo、商品抠图时非常实用。
另外,drop-shadow 的语法和 box-shadow 类似,但没有 spread 参数,颜色也只支持单个。它适合做“轮廓阴影”,不适合做多层组合。所以一个复杂浮层上,我通常用 box-shadow 做结构阴影,用 drop-shadow 做局部元素的轮廓阴影。
3.3 用drop-shadow做图案发光,以及性能上的取舍
drop-shadow 还有一个常见场景:发光。发光本质上就是“所有方向均匀扩散的阴影”,把 offset 设为 0,模糊半径拉大,再用一个明亮的颜色,就能制造霓虹灯效果。这在图标、按钮、文字标题上非常出效果。
css复制.icon-glow {
filter: drop-shadow(0 0 8px rgba(66, 133, 244, 0.8));
}
不过需要注意性能。filter 会创建新的渲染层,大量元素同时使用大范围 drop-shadow,在低端设备或滚动场景里容易掉帧。我踩过的坑是:一个长列表里每个 item 都挂了 drop-shadow,页面滚动时明显卡顿。后来改成只在 hover 时才加 filter,或者直接用 box-shadow 代替,滚动立刻恢复流畅。
经验是:静态装饰、小面积元素、hover 触发的发光,用 drop-shadow 没问题;大面积常驻阴影,优先用 box-shadow 的模糊层。这个取舍在移动端尤其重要。
4. 阴影动效:把“抬升感”“按压感”“呼吸感”做出来
4.1 hover抬升:阴影变化是位移和模糊的组合
交互反馈里,阴影的变化比颜色变化更有“物理感”。最典型的 hover 抬升,本质是模拟“用户把手指靠近物体,物体被吸附起来”的效果。要做到这一点,transform 和 box-shadow 必须同步变化。
我建议给两个属性都加 transition,并保持时长一致。transform 控制位移,box-shadow 控制光影,两者节奏一致才有整体感。
css复制.btn {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
这个思路也适合用在产品卡片、导航菜单、目录树上。核心原则就是:offset-y 的绝对值越大,说明物体离背景越远,blur-radius 和透明度也要跟着调整,否则会制造出“物体悬空但影子没变”的违和感。
4.2 按压感:inset 的妙用
和悬浮相对的是按压。用户点击按钮时,按钮应该“陷下去”,而不是“浮起来”。box-shadow 的 inset 关键字就是干这个的。内阴影把投影方向反过来,光线从元素内部“挖”出了一块暗区,配合 translateY 向下位移,按压感立刻就有了。
css复制.btn:active {
transform: translateY(1px);
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.15),
0 1px 2px rgba(0, 0, 0, 0.05);
}
注意 inset 0 2px 4px 和位移方向是配套的。按钮被按下时,顶部会被环境光遮住,形成一条内阴影,所以内阴影的 offset-y 用正值(向下)。如果想让按钮看起来是凸起的,可以用 inset 0 -2px 4px 配合亮色,制造“底部受光”的立体感。
内阴影还能做一件事:雕刻感。给文字容器加一层微弱的 inset 阴影,文字会呈现“刻进去”的效果,很适合做木纹按钮、金属质感的标签。
4.3 呼吸光圈与金光效果:多层阴影配合动画
这个技巧来自做营销页时的一个需求:让按钮产生“呼吸感”,像是有光在表面流动。实现原理不复杂,用多层 box-shadow 叠加一个较大的模糊半径,再让透明度循环变化即可。
css复制.breathe-btn {
box-shadow:
0 2px 12px rgba(255, 200, 100, 0.4),
0 0 0 0 rgba(255, 200, 100, 0.6);
animation: breathe 2s ease-in-out infinite;
}
@keyframes breathe {
0% {
box-shadow:
0 2px 12px rgba(255, 200, 100, 0.4),
0 0 0 0 rgba(255, 200, 100, 0.6);
}
70% {
box-shadow:
0 2px 12px rgba(255, 200, 100, 0.2),
0 0 0 16px rgba(255, 200, 100, 0);
}
100% {
box-shadow:
0 2px 12px rgba(255, 200, 100, 0.4),
0 0 0 0 rgba(255, 200, 100, 0.6);
}
}
第二层阴影用 0 0 0 0 到 0 0 0 16px 的扩展,配合透明度从 0.6 衰减到 0,会形成一圈向外扩散又消失的光圈,这就是“呼吸”的来源。如果想做金光闪闪的效果,把颜色换成渐变不可能,box-shadow 只支持纯色,但可以把几层不同颜色的阴影叠加,比如金色加暖白,再配合慢速旋转的背景动效,视觉上会有流光感。
这类动效我建议只在 C 端营销页和引导按钮上用,B 端表单页天天有入口按钮呼吸,用户会疯掉。
5. 把阴影工程化:设计变量、兼容性排查与常见暗坑
5.1 用CSS变量管理阴影层级
当项目里阴影数量多起来之后,直接在各组件里写死 box-shadow 一定是灾难。你很难保证所有卡片都用了同一套光源参数,也很难在改版时一键调整。解决方案是把它收敛到 CSS 变量里,当成设计变量管理。
css复制:root {
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 4px 12px rgba(0, 0, 0, 0.04);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06), 0 12px 32px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08), 0 24px 64px rgba(0, 0, 0, 0.12);
--shadow-focus: 0 0 0 3px rgba(66, 133, 244, 0.3);
}
层级命名建议用 xs/sm/md/lg 而不是 “卡片阴影”“弹窗阴影”,因为阴影是相对概念,不同组件可以用同一层级,但语义绑定到组件上就会造成大量重复变量。这样抽取之后,组件里就是一行:box-shadow: var(--shadow-md);
这里还要提一句 --shadow-focus 这种 focus ring 阴影。可访问性要求键盘操作时焦点可见,用 box-shadow 加一个不偏移的扩散阴影当描边,比 outline 好看,也不会挤占布局。这个变量几乎每个组件都能用到。
5.2 兼容性注意与边界情况
小程序里的 CSS 阴影兼容性有些特别。某些安卓 WebView 对 inset 支持不完整,尤其是 inset 和内层模糊组合时,可能出现阴影不渲染或渲染成实色块的情况。我的做法是:涉及小程序端时,先用一个透明度很低的纯色块当“降级阴影”,再叠加真正的 box-shadow,避免不渲染时出现完全没有层级感的极端情况。
另外,Safari 对 box-shadow 和 border-radius 的裁剪行为和其他浏览器有细微差别:在旧版本 Safari 里,圆角容器内阴影会溢出到圆角外部,看起来有一圈毛边。解决方案是加一句 overflow: hidden,或者在阴影颜色上进一步降低透明度。
5.3 一个不建议用的场景:大面积模糊阴影的性能问题
box-shadow 的模糊处理是纯 CPU/GPU 计算,不是简单的像素填充。一个大面积、大模糊半径、高透明度的阴影,在滚动或动画时会造成明显重绘开销。我遇到过一个问题:背景上有好几张大卡片,每张都叠了 0 24px 64px 的大模糊阴影,页面滚动时帧率掉到 30fps 以下。
这种场景的优化思路有三个:
- 用一个更大的、预先扁平化的背景渐变模拟阴影,而不是实时计算模糊;
- 用伪元素
::before放一个静态阴影位图,只对透明度做动画; - 降低大阴影层使用频率,只让需要突出层级的关键浮层用大阴影。
我自己更倾向于第二种方案:把模糊阴影画在一个伪元素上,hover 时只改透明度,避免触发 box-shadow 重绘。这个方案在手势交互多的页面里效果尤其明显。
写在最后:阴影颜色的小技巧
最后分享一个我个人调阴影的小习惯:阴影颜色不用纯黑,也不只用一种透明度。我会根据卡片背景稍微调整色调,比如白色卡片上的阴影偏向 rgba(15, 23, 42, 0.06),深色模式下则偏向 rgba(0, 0, 0, 0.4)。深色背景里的阴影比浅色背景更难做,因为黑色阴影会沉进黑色背景里,这时候更大的模糊半径和更高的透明度反而比纯黑更有效。
还有一点:阴影真的不是越多越好。层级感靠的是“对比”而不是“数量”,一个页面上如果所有东西都在浮,那没有东西是真正浮起来的。留几个不做阴影的平面元素,反而能衬托出需要强调的浮层。这是我从设计系统项目里得到的最大体会——克制比堆料更高级。
