Layered box-shadow CSS: offset, blur, spread, and inset
Published: 2026-09-05
How CSS box-shadow offset, blur, spread, color, and inset work—and how stacking layers builds soft cards, glows, and pressed UI without images.
box-shadow paints one or more shadows for a box—outside (drop) or inside (inset)—without extra markup or PNGs. Each shadow has offset, blur, spread, and color. Most polished UI shadows are layered: a soft wide layer plus a tighter darker one, sometimes with an inset highlight for “pressed” controls.
Anatomy of one shadow
The full form is:
box-shadow: inset? offset-x offset-y blur-radius? spread-radius? color?;
| Part | Role | Typical values |
|---|---|---|
inset (optional keyword) |
Shadow draws inside the padding edge | omit for outer; add for wells / pressed |
| Offset X / Y | How far the shadow shifts | 0 4px (down), -2px 0 (left) |
| Blur | Soft edge; 0 is a hard silhouette |
8px–24px for cards |
| Spread | Grow (positive) or shrink (negative) before blur | 0, 2px, or -4px |
| Color | Usually semi-transparent black or brand | rgb(0 0 0 / 0.15), #00000026 |
Omitted blur and spread default to 0. Color defaults to the element’s color (currentColor)—so always set an explicit color for predictable UI tokens.
/* Single outer shadow */
box-shadow: 0 8px 24px rgb(0 0 0 / 0.12);
/* Inset (inner) shadow */
box-shadow: inset 0 2px 4px rgb(0 0 0 / 0.2);
Offset, blur, and spread in practice
Offset places the shadow relative to the box. Positive Y reads as light from above (shadow falls down)—the usual card lift. Large X/Y with little blur looks like a second copy of the box; keep offsets modest unless you want that effect.
Blur is the soft radius. Doubling blur without changing opacity often looks “floatier” but washed out—pair larger blur with slightly lower opacity, or stack a second tighter layer so the silhouette stays readable.
Spread expands or contracts the shadow before blur. Positive spread makes a thicker halo (good for soft glows). Negative spread pulls the shadow inward so blur does not spill past the box as much—useful when a card sits near edges or other cards and you do not want muddy overlap.
Mental model:
- Take the box’s border box (or inset edge).
- Apply spread (grow/shrink).
- Apply blur.
- Shift by offset X/Y and paint with the color (respecting stacking and
border-radius).
Shadows follow the element’s border-radius; a pill or squircle casts a matching soft edge. Pair radius work with a live preview when corners matter—see related CSS generators on LocalTools when you tune both.
Layering: why one shadow is rarely enough
Browsers accept a comma-separated list. Shadows are painted from first listed = topmost among the shadows (still behind the box’s background/border as a group). Design systems often stack two or three outer layers:
.card {
box-shadow:
0 1px 2px rgb(0 0 0 / 0.06),
0 8px 24px rgb(0 0 0 / 0.08);
}
| Pattern | Layers | Use |
|---|---|---|
| Soft card | Small tight + larger soft | Elevation without a hard rim |
| Brand glow | Soft tinted outer (± slight inset) | Focus rings, selected tiles |
| Pressed / inset | One or two inset layers |
Inputs, toggles, recessed panels |
| Neumorphic-ish | Light inset + dark inset (or outer pair) | Skeuomorphic panels—easy to overdo |
Mixing outer and inset in one declaration is valid:
.panel {
box-shadow:
0 4px 16px rgb(0 0 0 / 0.1),
inset 0 1px 0 rgb(255 255 255 / 0.4);
}
The outer lift and the top highlight read as depth on light surfaces. On dark themes, flip the idea: lighter outer glow, darker inset—and check that text still meets contrast on the face of the control (WCAG contrast ratios).
Color and opacity
Pure #000 at full opacity looks harsh. Prefer alpha so the background shows through:
- Neutral elevation: black or near-black at 6%–20% opacity.
- Colored glow: brand HEX/HSL/OKLCH with alpha—convert and preview formats in the Color Converter if your tokens use different notations (HEX, RGB, HSL, and OKLCH).
- Dark mode: grey or white with low alpha often beats black on near-black surfaces.
Opacity is part of the color, not a separate box-shadow parameter. Changing only blur while keeping the same alpha can make the shadow look stronger or weaker because more pixels are covered softly.
Pitfalls and tips
- Performance. Huge blur radii and many layers on large or frequently animated elements cost more to paint. Prefer modest stacks; animate
opacityor a CSS variable that scales offsets rather than thrashing giant blurs every frame. filter: drop-shadow()follows the alpha shape of the element (good for irregular PNGs/SVGs).box-shadowfollows the CSS box. Do not confuse them when debugging “why doesn’t my icon cast a shadow?”- Spread ≠ blur. Spread changes size; blur changes softness. Fixing “too big” with only blur often muddies edges—nudge spread or opacity instead.
- Inset and overflow. Inset shadows clip to the padding box;
overflow: hiddenon ancestors can also clip outer shadows. - Focus visibility. A decorative glow is not a substitute for a visible
:focus-visibleoutline that passes non-text contrast. - Tokens. Store full
box-shadowstrings (or layered custom properties) so light/dark themes can swap stacks without hunting magic numbers.
Why tune shadows locally
Brand colors, elevation tokens, and inset recipes are easy to paste into online generators. A client-side builder keeps those values in your tab—same privacy habit as other LocalTools utilities (Why “local only” matters for developer tools).
Try it on LocalTools
Open the CSS box-shadow generator:
- Start from a preset (soft, medium, large, indigo glow, or inset) or the default layer.
- Tune offset X/Y, blur, spread, color, and opacity; toggle inset per layer.
- Add layers for stacked elevation; switch the preview surface (light, dark, checker) to judge contrast.
- Copy the full
box-shadowdeclaration into your CSS or design tokens.
Nothing is uploaded; the CSS updates as you edit.
Related reading
- CSS box-shadow generator — layered shadows with live preview
- Color Converter — HEX / RGB / HSL / OKLCH for shadow colors
- HEX, RGB, HSL, and OKLCH
- WCAG contrast ratios: AA vs AAA
- CSS
clamp()and fluid type - Why “local only” matters for developer tools