A single box-shadow looks flat. Multiple layered shadows look real. Real-world light creates multiple shadow components at different distances and intensities. CSS lets you replicate this by comma-separating shadow values. Here is exactly how to use multiple shadows, all-sides shadows, and advanced techniques like neumorphism.
Separate each shadow with a comma. They render in order — first shadow on top:
box-shadow:
0 1px 2px rgba(0,0,0,0.07),
0 4px 8px rgba(0,0,0,0.07),
0 12px 24px rgba(0,0,0,0.07);
Three layers: a tight contact shadow (1px), a medium ambient shadow (4px), and a wide environmental shadow (12px). Each has low opacity (0.07) so they add up to a natural-looking depth without any single layer being heavy.
The simplest technique — zero offset, blur only:
box-shadow: 0 0 15px rgba(0,0,0,0.2);
With h-offset: 0 and v-offset: 0, the shadow radiates equally from all edges. The blur radius controls how far it extends. This works for:
Add spread for more control: box-shadow: 0 0 15px 5px rgba(0,0,0,0.15); — the spread (5px) expands the shadow base before blur is applied.
The most important technique in modern CSS shadows. Three layers with increasing offset and blur create natural-looking elevation:
Layer 1 — Contact shadow (tight, dark)
0 1px 1px rgba(0,0,0,0.08)
Simulates where the element "touches" the surface. Very small offset and blur. Adds definition.
Layer 2 — Ambient shadow (medium)
0 4px 8px rgba(0,0,0,0.08)
The main visible shadow. Mid-range offset and blur. Provides the primary sense of elevation.
Layer 3 — Environmental shadow (wide, soft)
0 16px 32px rgba(0,0,0,0.06)
Large offset and blur, very low opacity. Creates the sense that light is diffusing around the element from a distant source.
Combined:
box-shadow:
0 1px 1px rgba(0,0,0,0.08),
0 4px 8px rgba(0,0,0,0.08),
0 16px 32px rgba(0,0,0,0.06);
This is the shadow system used by Stripe, Tailwind UI, and most premium design systems. It looks more realistic than any single shadow because it mimics how light actually behaves.
Build a reusable elevation system with CSS custom properties:
:root {
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 1px 3px rgba(0,0,0,0.1), 0 4px 8px rgba(0,0,0,0.08);
--shadow-lg: 0 1px 3px rgba(0,0,0,0.08), 0 8px 16px rgba(0,0,0,0.08), 0 24px 48px rgba(0,0,0,0.06);
--shadow-xl: 0 2px 4px rgba(0,0,0,0.08), 0 12px 24px rgba(0,0,0,0.1), 0 32px 64px rgba(0,0,0,0.08);
}
Apply consistently: cards get --shadow-md, modals get --shadow-xl, buttons get --shadow-sm. The scale creates visual hierarchy through consistent depth.
| Scenario | Shadow Count | Performance Impact | Recommendation |
|---|---|---|---|
| Static card | 2-3 layers | ✓ Negligible — renders once | Use as many as design needs |
| Hover transition | 2-3 layers | ~Moderate — repaint per frame | Keep transitions under 300ms |
| Scroll animation | 1-2 layers | ~Moderate — frequent repaints | Prefer pseudo-element opacity trick |
| Animated element (JS) | 1 layer | ~Frequent repaints per frame | Use transform + opacity instead |
| Decorative static (no interaction) | 5-10 layers | ✓ Single paint, then cached | No practical limit for static |
The rule: static shadows are cheap regardless of count. Animated shadows are expensive per frame. For hover effects, the pseudo-element trick avoids the repaint cost:
.card { position: relative; }
.card::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 12px 32px rgba(0,0,0,0.2);
opacity: 0;
transition: opacity 0.25s ease;
}
.card:hover::after { opacity: 1; }
The shadow is always rendered (cached). Only the opacity animates — which is GPU-composited and does not trigger repaint.
Neumorphism creates the illusion of elements extruding from the surface using two opposing shadows:
background: #e0e0e0;
box-shadow:
8px 8px 16px rgba(0,0,0,0.2),
-8px -8px 16px rgba(255,255,255,0.7);
The dark shadow (bottom-right) creates depth. The light shadow (top-left) creates a highlight, as if light is hitting the raised edge. The background must match the surrounding surface — neumorphism breaks if the element background differs from the page.
Pressed / inset neumorphism:
box-shadow:
inset 6px 6px 12px rgba(0,0,0,0.2),
inset -6px -6px 12px rgba(255,255,255,0.7);
Same technique with inset. Creates a concave, pressed-in appearance. Use for active button states or toggle switches in the "on" position.
Glassmorphism combines a light shadow with a translucent background and blur:
.glass-card {
background: rgba(255, 255, 255, 0.1);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
border-radius: 16px;
}
The shadow grounds the glass panel. Without it, the frosted glass looks like it is flat on the surface. A soft, wide shadow (large blur, low opacity) works best — sharp shadows conflict with the ethereal glass aesthetic.
Note: backdrop-filter has performance implications on mobile devices with lower-end GPUs. Test on real devices, not just desktop browsers.
Long shadow is a retro flat-design effect built from many 1px-offset shadows stacked diagonally:
box-shadow:
1px 1px 0 #bbb,
2px 2px 0 #bbb,
3px 3px 0 #bbb,
4px 4px 0 #bbb,
5px 5px 0 #bbb,
/* ... continue to 50-100px */
50px 50px 0 #bbb;
Writing this by hand is impractical. Use a CSS preprocessor (Sass or PostCSS) with a loop to generate the values. Or use a shadow generator that supports long shadow presets.
Long shadows look striking on hero sections and large headings. Avoid on small elements or interactive components — the visual weight is too heavy for buttons or cards.
CSS transitions interpolate each shadow in the list by position. Critical rule: both states must have the same number of shadows.
/* Base: 3 shadows */
box-shadow:
0 1px 2px rgba(0,0,0,0.1),
0 4px 8px rgba(0,0,0,0.08),
0 0 0 transparent; /* placeholder for 3rd */
/* Hover: 3 shadows */
box-shadow:
0 2px 4px rgba(0,0,0,0.1),
0 8px 16px rgba(0,0,0,0.12),
0 24px 48px rgba(0,0,0,0.08);
The base has a transparent third shadow as a placeholder. On hover, it transitions to a visible third layer. If the counts do not match, the browser cannot interpolate and the change is instant (no smooth transition).
| Technique | Shadow Count | CSS Pattern | Use Case |
|---|---|---|---|
| All-sides even | 1 | 0 0 blur rgba() | Modals, featured cards |
| Layered depth | 2-3 | Increasing offset + blur per layer | Cards, panels, design systems |
| Neumorphism | 2 | Opposite offsets, light + dark | Soft UI, toggle buttons |
| Glassmorphism | 1 | Soft shadow + backdrop-filter | Frosted panels, overlays |
| Long shadow | 50-100 | 1px increments, same color | Hero text, flat design accents |
| Border ring | 1 | 0 0 0 spread color | Focus indicators, selection |
| Animated | 2-3 (matched count) | Transition with same shadow count | Hover effects, interactions |
Build layered box-shadow CSS visually — adjust every value, copy instantly.
Open Box Shadow Generator