CSS box-shadow 从入门到拟物:多层阴影、内阴影与渲染性能CSS box-shadow From Basics to Skeuomorphism: Layered Shadows, Inset Shadows and Rendering Performance
`box-shadow` 是前端最常用的视觉工具之一,但五个参数的含义、多层叠加的技巧和性能陷阱并不总是被讲清楚。本文从语法拆解到拟物质感,再到什么时候该用图片代替阴影。`box-shadow` is one of the most-used visual tools in frontend, but the meaning of its five parameters, multi-layer stacking techniques and performance pitfalls are not always clearly explained. This article goes from syntax breakdown to skeuomorphic texture, and covers when to replace shadows with images.
语法拆解:五个值分别是什么Syntax breakdown: what each of the five values means
`box-shadow` 的完整语法是:`box-shadow: offset-x offset-y blur-radius spread-radius color inset;`。我见过很多人只会写 `box-shadow: 0 2px 4px rgba(0,0,0,0.1)`,对后面两个值和 `inset` 完全不了解,其实这正是阴影质感的关键。The full syntax of `box-shadow` is: `box-shadow: offset-x offset-y blur-radius spread-radius color inset;`. I've seen many people who only write `box-shadow: 0 2px 4px rgba(0,0,0,0.1)` and have no idea about the last two values or `inset` — yet those are exactly what make shadows feel polished.
`offset-x` 和 `offset-y` 是阴影的水平和垂直偏移,正值向右向下,负值向左向上。`blur-radius` 是模糊半径,值越大阴影越柔和,0 就是硬边阴影。`spread-radius`(扩散半径)是最容易被忽略的参数:正值让阴影变大,负值让阴影缩小。我在做"卡片悬浮"效果时,常用 `spread-radius: 4px` 让阴影比卡片本身大一圈,悬浮感更强;而做"内凹"效果时用负值缩小阴影,让它看起来像是嵌进去的。`offset-x` and `offset-y` are horizontal and vertical offsets: positive values go right and down, negative go left and up. `blur-radius` is the blur radius — larger means softer, 0 means a hard edge. `spread-radius` is the most overlooked parameter: positive values enlarge the shadow, negative shrink it. For "card hover" effects I often use `spread-radius: 4px` to make the shadow larger than the card itself, strengthening the floating feel; for "inset" effects, negative values shrink the shadow so it looks embedded.
`color` 用 rgba 是最佳实践,因为可以控制透明度。纯黑 `#000` 的阴影在浅色背景上太生硬,`rgba(0,0,0,0.1)` 到 `rgba(0,0,0,0.25)` 是最常用的范围。还有一个进阶技巧:阴影颜色不一定是黑色,可以用和背景同色系的深色,比如在蓝色背景上用 `rgba(20,40,80,0.3)`,这样阴影和画面更融合,不会有"浮在上面"的割裂感。Using rgba for `color` is best practice because you control opacity. Pure black `#000` shadows look too harsh on light backgrounds; `rgba(0,0,0,0.1)` to `rgba(0,0,0,0.25)` is the most common range. An advanced trick: shadow colour doesn't have to be black — use a dark shade from the same family as the background, e.g. `rgba(20,40,80,0.3)` on a blue background. This blends the shadow with the scene and avoids the "floating on top" disconnect.
`inset` 关键字把外阴影变成内阴影,阴影绘制在元素内部而非外部。这个单独放后面讲。The `inset` keyword turns an outer shadow into an inner one, drawn inside the element rather than outside. I'll cover that separately.
多层阴影与拟物质感Layered shadows and skeuomorphic texture
单层阴影看起来总是"平"的,因为真实世界的阴影从来不是一层:有紧贴物体的暗部(接触阴影),有向外扩散的柔和半影,还有环境光反射带来的微弱亮边。CSS 里用逗号分隔多层 `box-shadow`,就能模拟这种真实感。Single-layer shadows always look "flat" because real-world shadows are never one layer: there's the dark contact shadow tight against the object, the soft penumbra spreading outward, and a faint bright edge from ambient light reflection. In CSS, comma-separated multiple `box-shadow` layers simulate this realism.
我常用的"拟物卡片"阴影是三层叠加:My go-to "skeuomorphic card" shadow is three layers:
第一层是紧贴卡片的小阴影(模拟接触阴影),第二层是中等范围的柔和阴影,第三层是大范围的淡阴影(模拟环境漫射)。三层的透明度递增、模糊半径递增、偏移递增,叠在一起就有了"卡片真的浮在桌面上"的质感。这比单层 `0 8px 24px rgba(0,0,0,0.15)` 自然得多。The first layer is a small shadow tight against the card (contact shadow), the second is a medium soft shadow, the third is a large faint shadow (ambient diffusion). All three increase in opacity, blur radius and offset — stacked together they give a "card truly floating on a desk" feel. Much more natural than a single `0 8px 24px rgba(0,0,0,0.15)`.
苹果的设计语言里还有一个经典技巧:在阴影上方加一层极淡的内发光(用 `inset` 阴影),模拟物体边缘被环境光照亮的效果。代码如下:Apple's design language has a classic trick: add a very faint inner glow (using `inset` shadow) above the outer shadows to simulate the object edge being lit by ambient light:
第一层 `inset 0 1px 0` 在按钮顶部内侧画一条 1px 的半透明白线,模拟高光;后面两层是外阴影。这个组合在拟物按钮上效果非常好,是 iOS 6 时代按钮的标准做法,现在在一些需要"质感"的场景里依然好用。The first layer `inset 0 1px 0` draws a 1px semi-transparent white line on the button's top inner edge (highlight); the next two are outer shadows. This combination works beautifully on skeuomorphic buttons — it was standard in the iOS 6 era and still works well in scenes that need "texture."
多层阴影的一个注意点:层叠顺序是"先写的在上层",所以内阴影要写在最前面,外阴影按从近到远的顺序写。One note with layered shadows: stacking order is "first written is on top," so inner shadows go first, and outer shadows are written from nearest to farthest.
内阴影 inset 的妙用Clever uses of inset shadows
`inset` 把阴影画在元素内部,最直接的用途是做"内凹"效果。一个常见的输入框内凹样式:`inset` draws the shadow inside the element; the most direct use is an "inset/concave" effect. A common inset input style:
阴影从顶部向内投射,看起来输入框是"陷进去"的。这在拟物风格的表单里很常见,比单纯用边框更有质感。The shadow projects inward from the top, making the input look "sunken." Common in skeuomorphic forms — more texture than a plain border.
但 `inset` 的用处远不止内凹。我最喜欢的一个技巧是用 `inset` 做"单侧边框"——当你需要一条比 `border` 更柔和的分隔线时:But `inset` is useful for far more than concavity. A favourite trick of mine: using `inset` for "one-sided borders" when you need a divider softer than `border`:
这在元素顶部内侧画一条 1px 的淡线,效果和 `border-top: 1px solid rgba(0,0,0,0.08)` 几乎一样,但有一个关键优势:`inset` 阴影不占布局空间,不会像 `border` 那样改变元素尺寸(除非用 `box-sizing: border-box`)。在需要精确控制元素尺寸的网格布局里,用 `inset` 做分隔线比 `border` 更省心。This draws a 1px faint line on the element's top inner edge — nearly identical to `border-top: 1px solid rgba(0,0,0,0.08)`, but with one key advantage: `inset` shadows don't take up layout space and don't change element dimensions like `border` does (unless you use `box-sizing: border-box`). In grid layouts where element dimensions must be precise, `inset` dividers are less hassle than `border`.
另一个妙用是"渐变边框"。直接用 `border` 只能做纯色边框,但用多层 `inset` 阴影可以模拟渐变边框:Another clever use is "gradient borders." `border` only does solid colours, but multiple `inset` shadows can simulate a gradient border:
四条边分别用不同颜色的 `inset` 阴影,模拟出上下渐变、左右渐变的边框效果。当然,更标准的渐变边框做法是 `border-image` 或背景渐变加 `padding`,但 `inset` 阴影的好处是不需要额外的 DOM 结构或背景层,简单场景下够用。Four sides use different-coloured `inset` shadows to simulate top-bottom and left-right gradient borders. Of course, the more standard approach is `border-image` or background gradient plus `padding`, but `inset` shadows have the benefit of needing no extra DOM structure or background layer — enough for simple cases.
渲染性能:什么时候会掉帧Rendering performance: when frames drop
`box-shadow` 的渲染开销比大多数人想象的大。阴影是在合成阶段(compositing)绘制的,浏览器需要对阴影区域做高斯模糊,这是一个计算密集型操作。当阴影模糊半径大(如 50px 以上)、元素多(如一个页面有 50 个带阴影的卡片)、且元素在动画中(如滚动、hover 过渡)时,性能问题就会显现。`box-shadow` is more expensive to render than most people realise. Shadows are drawn during the compositing phase, and the browser applies a Gaussian blur to the shadow area — a computationally intensive operation. Performance issues show up when blur radius is large (50px-plus), there are many elements (50 shadowed cards on a page), and elements are animating (scrolling, hover transitions).
我在一个电商列表页遇到过真实案例:每个商品卡片都有 `box-shadow: 0 10px 40px rgba(0,0,0,0.15)`,一屏 20 个卡片,滚动时在中端 Android 上帧率掉到 40fps 以下。排查后发现是阴影的模糊半径太大,且卡片在滚动中不断重绘。解决方案有两个:一是把模糊半径从 40px 降到 20px,视觉差异不大但性能提升明显;二是给卡片加 `transform: translateZ(0)` 触发硬件加速,让阴影在独立图层上合成,不随滚动重绘。I hit a real case on an e-commerce list page: every product card had `box-shadow: 0 10px 40px rgba(0,0,0,0.15)`, 20 cards per screen, and scroll frame rate dropped below 40fps on mid-range Android. Investigation showed the blur radius was too large and cards repainted constantly during scroll. Two fixes: first, reduce blur radius from 40px to 20px — minimal visual difference, noticeable performance gain; second, add `transform: translateZ(0)` to trigger hardware acceleration, compositing shadows on separate layers so they don't repaint with scroll.
还有一个更彻底的方案:对于静态的、不需要动画的大阴影,可以用一张 PNG 阴影图代替 CSS 阴影。图片只解码一次,之后只是纹理上传,比实时高斯模糊便宜得多。我在一些营销页的 hero 区域用过这个方案,把复杂的多层阴影导出成 1px 高的渐变图(用 `background-repeat` 平铺),性能和视觉都达标。A more thorough solution: for static, non-animating large shadows, replace CSS shadows with a PNG shadow image. The image decodes once and is just a texture upload afterward — much cheaper than real-time Gaussian blur. I've used this on hero sections of marketing pages, exporting complex multi-layer shadows as a 1px-tall gradient image (tiled with `background-repeat`), meeting both performance and visual targets.
`inset` 阴影的性能开销通常比外阴影小,因为它的绘制区域被限制在元素内部,且不需要考虑元素外部的混合。但如果 `inset` 阴影的模糊半径也很大,同样会有性能问题。`inset` shadows are usually cheaper than outer shadows because the draw area is confined to the element interior and no external blending is needed. But if the inset blur radius is also large, performance suffers similarly.
最后一个建议:用 Chrome DevTools 的 Performance 面板录制滚动,看 "Rendering" 阶段的耗时。如果 "Paint" 或 "Composite" 占比高,且元素带大阴影,基本可以确定是阴影的锅。用本站的 CSS 阴影生成器可以实时调整参数并预览效果,找到视觉和性能的平衡点比手写代码高效得多。Final tip: use Chrome DevTools' Performance panel to record scrolling and check "Rendering" time. If "Paint" or "Composite" is high and elements carry large shadows, shadows are almost certainly the culprit. Our CSS shadow generator lets you adjust parameters and preview in real time — finding the visual/performance balance is far more efficient than hand-writing code.