The box-shadow Syntax
box-shadow: [inset] offset-x offset-y [blur] [spread] color;
- offset-x / offset-y: Shadow position. Positive x goes right, positive y goes down.
- blur: How soft the shadow is. 0 = hard edge.
- spread: How much the shadow expands beyond the element. Can be negative.
- inset: Moves the shadow inside the element.
- You can stack multiple shadows separated by commas.
Technique 1: Layered Shadows for Real Depth
A single shadow looks flat and generic. Layering 2–3 shadows at different blur radii and opacities produces the depth you see in professional design systems:
/* Material Design-inspired elevation */
.card--elevated {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.30),
0 4px 8px rgba(0, 0, 0, 0.25),
0 16px 32px rgba(0, 0, 0, 0.20);
}
The smallest shadow adds the crisp edge detail; the largest creates the ambient shadow. Together they feel physically convincing.
Technique 2: Colored Glow Effects
Using a colored shadow with no offset creates a glow. Pair it with a spread for extra intensity:
/* Purple glow for a primary button */
.btn-primary {
background: #7c5cfc;
box-shadow:
0 0 0 0 transparent, /* ring-offset */
0 8px 24px rgba(124,92,252,0.45), /* ambient glow */
0 0 60px rgba(124,92,252,0.15); /* distant halo */
transition: box-shadow 0.3s ease;
}
.btn-primary:hover {
box-shadow:
0 0 0 0 transparent,
0 12px 32px rgba(124,92,252,0.65),
0 0 80px rgba(124,92,252,0.25);
}
Technique 3: Neumorphism (Soft UI)
Neumorphism creates an extruded-from-surface look using two shadows — one light, one dark — offset in opposite directions. The element's background must match the container background exactly:
:root { --bg: #e0e5ec; }
.neomorphic {
background: var(--bg);
border-radius: 16px;
box-shadow:
8px 8px 16px #b8bec7, /* dark shadow (bottom-right) */
-8px -8px 16px #ffffff; /* light shadow (top-left) */
}
/* Pressed/active state */
.neomorphic:active {
box-shadow:
inset 6px 6px 12px #b8bec7,
inset -6px -6px 12px #ffffff;
}
Accessibility warning: Neumorphism often produces very low contrast between the element and background. Always verify your contrast ratios pass WCAG AA — use StudioLimb's Contrast Checker.
Technique 4: Inset Shadows for Depth
Inset shadows create a sunken or engraved appearance, great for input fields, wells, and pressed button states:
/* Dark mode input field */
.input {
background: rgba(0, 0, 0, 0.3);
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.6),
inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}
.input:focus {
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.6),
0 0 0 2px rgba(124, 92, 252, 0.6);
}
Technique 5: Negative Spread for Tight Shadows
A negative spread radius shrinks the shadow, letting you create a shadow that only appears below the element (like it's floating above a surface) without spreading to the sides:
/* Shadow appears only below, not on sides */
.floating {
box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.5);
/* ^^^ negative spread */
}
Performance: box-shadow vs filter: drop-shadow()
box-shadow is GPU-accelerated in all modern browsers and is the right choice for most cases. filter: drop-shadow() follows the element's alpha channel (including transparent PNG cutouts) but is more expensive and shouldn't be used on animating elements. Stick with box-shadow unless you specifically need the shape-following behavior.
Transition Tip
Avoid animating box-shadow on scroll or hover for many elements simultaneously. Instead, animate opacity on a pseudo-element that has the target shadow — this is GPU-composited and far smoother.
/* Performant shadow hover animation */
.card {
position: relative;
transition: transform 0.2s ease;
}
.card::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.2s ease;
}
.card:hover {
transform: translateY(-4px);
}
.card:hover::after {
opacity: 1;
}
Technique 6: Box Shadow as a Border (Zero-Blur Trick)
Using box-shadow with zero blur and zero offset creates a border-like outline that doesn't affect layout (unlike border, which adds to the element's box model). This is useful for adding multiple "borders" or for creating focus rings without layout shift:
/* Doesn't affect layout, can be stacked */
.multi-border {
box-shadow:
0 0 0 2px #7c5cfc, /* inner ring */
0 0 0 5px rgba(124,92,252,0.2); /* outer glow ring */
}
/* Focus ring without layout shift */
.btn:focus-visible {
outline: none;
box-shadow:
0 0 0 2px #050508, /* offset gap */
0 0 0 4px #7c5cfc; /* visible ring */
}
Building a Shadow Design Token System
Professional design systems define shadows as named tokens, not one-off values. This ensures consistency across components and makes global updates trivial:
:root {
/* Elevation scale */
--shadow-xs: 0 1px 2px rgba(0,0,0,0.2);
--shadow-sm: 0 1px 2px rgba(0,0,0,0.2), 0 4px 8px rgba(0,0,0,0.15);
--shadow-md: 0 1px 2px rgba(0,0,0,0.25), 0 8px 16px rgba(0,0,0,0.2), 0 20px 40px rgba(0,0,0,0.15);
--shadow-lg: 0 2px 4px rgba(0,0,0,0.3), 0 16px 32px rgba(0,0,0,0.25), 0 40px 80px rgba(0,0,0,0.2);
/* Semantic shadows */
--shadow-card: var(--shadow-sm);
--shadow-modal: var(--shadow-lg);
--shadow-button: var(--shadow-xs);
/* Colored glows */
--shadow-primary-glow: 0 8px 24px rgba(124,92,252,0.4);
--shadow-danger-glow: 0 8px 24px rgba(239,68,68,0.4);
}
/* Usage */
.card { box-shadow: var(--shadow-card); }
.modal { box-shadow: var(--shadow-modal); }
.btn-cta { box-shadow: var(--shadow-primary-glow); }
Common Box Shadow Mistakes
1. Single shadow on everything
One shadow at one blur radius looks flat and generic — the same shadow Photoshop users have been applying since 2006. Use 2–3 layers at different blur radii. The extra two lines of CSS are always worth it.
2. Black shadows on dark backgrounds
Pure black shadows (rgba(0,0,0,X)) on dark backgrounds look muddy. Add a subtle hue that matches the surface color: a dark blue background should use dark blue-tinted shadows (rgba(10,10,30,X)).
3. Spread radius making shadows too large
A large positive spread pushes the shadow far beyond the element's edges, creating an ugly halo. Use spread at 0 or negative values for most cases. Reserve positive spread for intentional glow effects.
4. Animating box-shadow directly
Animating box-shadow forces the browser to repaint on every frame — expensive on complex pages. Use the pseudo-element opacity technique above for hover transitions instead.
5. Forgetting dark mode
Shadows that look great on white backgrounds are often invisible on dark backgrounds. Build your shadow tokens separately for light and dark modes, or use @media (prefers-color-scheme: dark) to swap them.
Frequently Asked Questions
What's the maximum number of box-shadow layers I should use?
3 layers is almost always sufficient and covers all real-world needs. Beyond that, the visual difference is imperceptible to most users and the performance cost isn't worth it. Google's Material Design uses exactly 3 elevation layers for its shadow system.
Can I use box-shadow on inline elements?
Yes, but it behaves differently — it renders on each line box separately when text wraps. For inline text highlights, consider background with a padding hack instead, as it gives you more control over the shape.
Is box-shadow or filter: drop-shadow() better for performance?
box-shadow is composited on the GPU in all modern browsers and is the better choice for most use cases. filter: drop-shadow() follows the element's alpha mask (useful for cutout PNGs), but it's repainted on the CPU and should not be used on animated elements.
How do I create a bottom-only shadow?
Use a negative spread radius to prevent the shadow from appearing on the sides: box-shadow: 0 20px 30px -15px rgba(0,0,0,0.5). The -15px spread pulls the shadow inward, making it appear only at the bottom.