Box Shadow Basics
The CSS box-shadow property adds one or more shadows to an element's border box. Each shadow definition takes up to six values in this order: horizontal offset, vertical offset, blur radius, spread radius, color, and the optional inset keyword. Understanding what each value does is the foundation for building any shadow effect.
/* offset-x | offset-y | blur | color */ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); /* offset-x | offset-y | blur | spread | color */ box-shadow: 4px 8px 16px 2px rgba(0, 0, 0, 0.1); /* Inset shadow — shadow inside the element */ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.12); /* Multiple shadows — comma-separated */ box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 8px 24px rgba(0,0,0,0.12);
The blur radius controls how soft the shadow edge is. A value of 0 creates a perfectly sharp shadow with a hard edge — useful for flat design retro effects. Higher values (12px–40px) create the soft, natural shadows used in most modern UI. The spread radius expands or contracts the shadow independently of blur — a negative spread value creates a shadow smaller than the element itself, which is the basis for the popular "underline" shadow effect.
The horizontal and vertical offsets control the direction the shadow is cast. A shadow with 0 0 offsets radiates equally in all directions, giving the impression of ambient light from directly above. A shadow with 0 4px looks like light coming from above and slightly behind.
Layered Shadow Technique
A single shadow looks flat because real light never produces a single clean shadow. Natural light produces multiple overlapping shadows — a tight, dark shadow from the direct light source and a wide, soft shadow from diffuse ambient light. Layering multiple CSS shadows replicates this physical reality.
The key is to make each individual shadow quite transparent, so the combined effect looks natural rather than heavy. Three layers is the sweet spot for most UI components:
/* Three-layer realistic shadow */
.card {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.04), /* tight edge definition */
0 4px 8px rgba(0, 0, 0, 0.06), /* primary elevation */
0 16px 32px rgba(0, 0, 0, 0.08); /* ambient depth */
}
/* Five-layer premium feel */
.card-premium {
box-shadow:
0 1px 1px rgba(0,0,0,0.025),
0 2px 4px rgba(0,0,0,0.04),
0 4px 8px rgba(0,0,0,0.05),
0 8px 16px rgba(0,0,0,0.06),
0 16px 32px rgba(0,0,0,0.07);
}
Each layer should have low opacity individually. The combined stacking creates the full visual weight. If you make each shadow fully opaque, the result looks like a cartoon drop shadow — heavy and unnatural. Start at 4–8% opacity per layer and adjust from there.
Building an Elevation System
Material Design popularised the concept of elevation — using shadow intensity to communicate visual hierarchy. Elements closer to the "surface" have minimal shadow; elements further away (modals, tooltips, floating buttons) have deeper, more diffuse shadows. You can implement this as a set of CSS custom properties:
:root {
--shadow-xs: 0 1px 2px rgba(0,0,0,0.05);
--shadow-sm:
0 1px 3px rgba(0,0,0,0.06),
0 2px 6px rgba(0,0,0,0.04);
--shadow-md:
0 2px 4px rgba(0,0,0,0.04),
0 6px 16px rgba(0,0,0,0.06);
--shadow-lg:
0 4px 8px rgba(0,0,0,0.04),
0 12px 32px rgba(0,0,0,0.08);
--shadow-xl:
0 8px 16px rgba(0,0,0,0.06),
0 24px 48px rgba(0,0,0,0.1);
}
/* Usage hierarchy */
.card { box-shadow: var(--shadow-sm); }
.card:hover { box-shadow: var(--shadow-lg); transition: box-shadow 0.2s ease; }
.dropdown { box-shadow: var(--shadow-md); }
.modal { box-shadow: var(--shadow-xl); }
.tooltip { box-shadow: var(--shadow-xs); }
.fab { box-shadow: var(--shadow-lg); }
This system keeps your shadows consistent across the entire project. When a designer wants to adjust the overall "lighting feel", you change the custom properties once and every component updates. It also makes dark mode easy — you can override the entire scale inside a .dark class or @media (prefers-color-scheme: dark) block.
Coloured and Brand Shadows
Using coloured shadows instead of black/grey creates a softer, more modern aesthetic — popularised by companies like Stripe, Linear, and Vercel. The technique uses a darker, more saturated version of the element's background colour as the shadow colour. This makes the shadow feel like it belongs to the element rather than being a generic grey cast.
/* Brand-coloured button shadow */
.btn-primary {
background: #7c6fff;
box-shadow: 0 4px 14px rgba(124, 111, 255, 0.4);
}
.btn-primary:hover {
box-shadow: 0 8px 24px rgba(124, 111, 255, 0.5);
transform: translateY(-2px);
}
/* Pink accent card */
.card-accent {
border-top: 3px solid #ff6fb0;
box-shadow: 0 8px 30px rgba(255, 111, 176, 0.15);
}
/* Green success state */
.alert-success {
background: #f0fdf4;
box-shadow: 0 4px 16px rgba(34, 197, 94, 0.15);
}
/* Dark card with subtle blue tint */
.card-dark {
background: #13131a;
box-shadow: 0 8px 32px rgba(100, 120, 255, 0.12);
}
Inset Shadows for Depth and Pressed States
The inset keyword reverses the shadow direction — instead of casting outward, the shadow falls inside the element. This creates the visual impression that the element is embedded in or pressed into the surface, rather than floating above it. Inset shadows are used for input fields, pressed button states, inner well effects, and neumorphism.
/* Recessed input field */
.input {
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.08);
border: 1px solid var(--border);
}
.input:focus {
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.04),
0 0 0 3px rgba(124, 111, 255, 0.2);
}
/* Pressed button state */
.btn:active {
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15);
transform: translateY(1px);
}
/* Neumorphism panel */
.neu-panel {
background: #e8e8f0;
box-shadow:
6px 6px 12px rgba(0, 0, 0, 0.12),
-6px -6px 12px rgba(255, 255, 255, 0.8);
}
/* Combined outer + inset for badge */
.badge-inset {
box-shadow:
0 2px 8px rgba(124,111,255,0.2), /* outer glow */
inset 0 1px 2px rgba(255,255,255,0.3); /* inner highlight */
}
Animating Box Shadows Correctly
Animating box-shadow directly triggers a repaint on every frame, which can cause jank on complex pages. The correct approach is to use a ::after pseudo-element that holds the target shadow, then animate its opacity. Opacity changes are compositor-friendly — they happen on the GPU without triggering layout or paint.
/* Performant shadow hover — animates opacity, not shadow */
.card {
position: relative;
box-shadow: var(--shadow-sm);
transition: transform 0.25s ease;
}
.card::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: var(--shadow-lg);
opacity: 0;
transition: opacity 0.25s ease;
pointer-events: none;
}
.card:hover {
transform: translateY(-4px);
}
.card:hover::after {
opacity: 1;
}
This technique gives you smooth 60fps shadow transitions with no jank. The card lifts on hover while the heavier shadow fades in simultaneously, creating a convincing floating effect.
Performance Tips
Box shadows are painted by the browser's rasterisation step. For most UI they are very fast, but a few practices can cause problems at scale:
- Large blur values on many elements — shadows with blur radii above 60px on dozens of visible elements can impact scroll performance on low-end devices. Apply large blur shadows only to fixed or sticky elements, not repeated cards.
- Animating box-shadow directly — triggers repaint every frame. Use the pseudo-element technique above instead.
- Shadows during scroll — if a card with a complex shadow is in a scrolling list, add
will-change: transformto promote it to its own compositing layer. This moves the shadow rendering off the main thread. - Mobile devices — apply a lighter shadow scale at small viewport sizes using a media query. Mobile GPUs have less headroom for complex blur calculations.
/* Reduce shadow complexity on mobile */
@media (max-width: 768px) {
:root {
--shadow-md: 0 2px 8px rgba(0,0,0,0.1);
--shadow-lg: 0 4px 16px rgba(0,0,0,0.12);
}
}
/* Promote scrolling shadow elements */
.card-list .card {
will-change: transform;
}
Build layered, coloured shadows visually
Multiple shadow layers, inset shadows, coloured shadows — preview everything live and copy the CSS.
Open Box Shadow Generator →