CSS Box Shadow: The Complete Guide With Live Examples
Master CSS box-shadow from syntax basics to layered effects, Tailwind utilities, and interactive live examples — everything you need to build depth into your UI.
What CSS Box Shadow Actually Does (And Why It's Not Just a Drop Shadow)
Box shadow is one of those properties you think you know until you actually need it to do something specific. The MDN definition is dry: it attaches one or more shadow effects to an element's frame. But the real picture is more interesting. You're painting one or more colour-filled copies of the element's bounding box, offset and blurred, stacked behind (or in front of) the element itself.
The basic syntax takes up to six values: box-shadow: offset-x offset-y blur-radius spread-radius color. Two of those are optional — spread defaults to 0, and color defaults to the element's current color value if you omit it. Then there's the inset keyword, which flips the whole thing inside the element boundary instead of outside it.
In practice, people use maybe 10% of what the property can do. Most sites slap a single 0 4px 6px rgba(0,0,0,0.1) on a card and call it a day. That's fine! But once you layer multiple shadows, mix inset with outset, or use spread creatively, you unlock effects that feel like they need JavaScript to exist.
Quick aside: spread-radius is the least understood value here. Positive spread expands the shadow's footprint beyond the element's actual dimensions. Set it to -4px and the shadow tucks inward slightly, giving you a softer edge with no colour bleed. Very useful for cards on white backgrounds.
The Full Syntax Broken Down With Real Numbers
Let's go concrete. Here's a layered shadow that looks like it's floating 12px off the page:
``css
.card {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.07),
0 2px 4px rgba(0, 0, 0, 0.07),
0 4px 8px rgba(0, 0, 0, 0.07),
0 8px 16px rgba(0, 0, 0, 0.07),
0 16px 32px rgba(0, 0, 0, 0.07);
}
``
Each layer doubles the previous blur and offset. The opacity stays constant at 7%. That geometric progression is what makes the shadow feel physically believable instead of flat and stamped-on. You can adjust the base opacity down to 4% for very light backgrounds or up to 12% for dark surfaces.
Why five layers and not one blurred shadow? Because a single 0 16px 32px rgba(0,0,0,0.35) has a visible gradient falloff that looks artificial — the shadow is too dark near the element and too abrupt at the edge. Layering lower-opacity shadows approximates the natural penumbra of a real light source.
Worth noting: the order in the comma-separated list matters for stacking. First shadow listed is on top. If you mix outset and inset shadows on the same element, they render independently and both show up — no overwriting.
One more thing — box-shadow doesn't affect layout. It doesn't push sibling elements, doesn't change scroll bounds, nothing. It's purely visual. That's usually what you want, but if you need the shadow to physically take up space (rare, but happens in tight print layouts), you'd need filter: drop-shadow() instead, which *does* participate in overflow.
Inset Shadows: Building Depth From the Inside
Add inset before the offset values and the shadow renders inside the element's border edge rather than outside. Suddenly you've got a pressed-in look — great for inputs, toggles, and that neumorphic indented-surface feel.
/* Pressed button state */
.btn:active {
box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.2);
}
/* Neumorphic input field */
.input {
background: #e0e5ec;
box-shadow:
inset 6px 6px 10px rgba(163, 177, 198, 0.6),
inset -6px -6px 10px rgba(255, 255, 255, 0.8);
}That input example is neumorphism in a nutshell — a dark shadow on the top-left, a light shadow on the bottom-right, both inset. It creates the illusion the field is recessed into the surface. If that's a style you want to explore further, the neumorphism style guide on Empire UI has a full breakdown with interactive component demos.
Honestly, inset shadows are underused in standard design systems. Most developers reach for borders or background colour changes for interactive states, but a well-tuned inset shadow communicates pressure and state change in a way that feels tactile rather than just visual.
Tailwind CSS Utilities and When to Go Custom
Tailwind ships with five named shadow utilities since v3: shadow-sm, shadow, shadow-md, shadow-lg, shadow-xl, and shadow-2xl. They're all single-layer, vertically offset, and use a pre-baked rgba(0,0,0,N) colour. Fine for 80% of cases.
Where you hit the wall is coloured shadows, horizontal-only shadows, or anything layered. For those you're extending the config:
``js
// tailwind.config.js
module.exports = {
theme: {
extend: {
boxShadow: {
'glow-purple': '0 0 20px 4px rgba(139, 92, 246, 0.45)',
'card-float':
'0 1px 2px rgba(0,0,0,0.07), 0 4px 8px rgba(0,0,0,0.07), 0 16px 32px rgba(0,0,0,0.07)',
'inner-press': 'inset 0 3px 6px rgba(0,0,0,0.2)',
},
},
},
}
`
Then in your JSX: className="shadow-glow-purple hover:shadow-card-float transition-shadow duration-300"`. Clean.
That said, if you're iterating on a design and constantly tweaking values, dropping into a visual tool is way faster than editing config files. Our box shadow generator lets you tune all six parameters with sliders and copies the CSS or Tailwind config snippet directly. Worth having open in a tab.
One pattern worth having in your toolkit: coloured shadows to match a button's background. A purple button with a 0 8px 24px rgba(139, 92, 246, 0.5) shadow feels dramatically more premium than the same button with a grey shadow. It's 2026 and flat buttons with no depth signal are starting to feel dated.
Layered and Animated Shadows for UI Effects
Layered shadows get genuinely interesting when you animate them. The trick is to pre-define both the resting and elevated states, then transition between them. CSS transitions handle box-shadow natively — just don't transition to none, which jumps instead of animates.
``css
.card {
transition: box-shadow 250ms ease, transform 250ms ease;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.08),
0 4px 12px rgba(0, 0, 0, 0.06);
}
.card:hover {
transform: translateY(-4px);
box-shadow:
0 4px 8px rgba(0, 0, 0, 0.08),
0 12px 28px rgba(0, 0, 0, 0.12),
0 24px 48px rgba(0, 0, 0, 0.06);
}
``
That translateY(-4px) paired with an expanded shadow sells the lift effect. The element moves up 4px while the shadow grows — exactly how a card would look under a fixed overhead light source when you pick it up.
Glow effects follow the same pattern. Set blur to a high value (24–48px), spread to a small positive value (2–4px), and use a saturated colour with 40–60% opacity. Pair this with :hover or a CSS animation for a pulsing glow:
``css
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px 4px rgba(99, 102, 241, 0.4); }
50% { box-shadow: 0 0 36px 8px rgba(99, 102, 241, 0.7); }
}
.glow-btn {
animation: pulse-glow 2s ease-in-out infinite;
}
``
Look, this exact pattern is what drives most of the effects you'd see on glassmorphism cards. If you're building that style, it's worth checking the glassmorphism components collection — they combine backdrop-filter, border opacity, and layered shadows in ways that are hard to get right from scratch.
Performance, Gotchas, and Browser Support
Box shadow is GPU-composited in every modern browser back to IE9, so performance is generally not an issue for static shadows. Animated shadows are a different story — they trigger repaints on every frame. If you're animating box-shadow on scroll or in a tight loop, you might see jank on lower-end mobile hardware.
The standard fix is to use opacity or transform to fake the animation instead of directly animating box-shadow. Pre-render the elevated shadow version as a pseudo-element, then transition its opacity on hover:
``css
.card { position: relative; }
.card::after {
content: '';
position: absolute; inset: 0;
box-shadow: 0 12px 40px rgba(0,0,0,0.2);
opacity: 0;
transition: opacity 250ms ease;
border-radius: inherit;
}
.card:hover::after { opacity: 1; }
``
That ::after trick keeps everything on the compositor thread. No repaints. Works especially well when you have many cards on screen simultaneously.
Worth noting: box-shadow doesn't clip to overflow: hidden. The shadow renders outside the element regardless of the parent's overflow setting. And if you're using will-change: transform elsewhere on the element, the browser creates a new stacking context — which can cause shadows to appear clipped by parent containers. The fix is to apply will-change to a wrapper instead of the element with the shadow.
Putting It Together: Choosing the Right Shadow for Your Style
Different design languages call for completely different shadow approaches. Realistic material design uses multiple low-opacity layered shadows with a slight warm or cool tint. Neobrutalism uses hard shadows with zero blur — literally 4px 4px 0 #000 — which you can explore more at the neobrutalism guide. Glassmorphism uses minimal, soft, large-radius shadows paired with blur and translucency.
The decision tree is actually pretty simple. Does your design have a defined light source direction? Use directional offset and skip the centered glow. Is it a dark-mode UI? Use rgba with higher opacity — dark shadows disappear against dark backgrounds and you need either lighter spread shadows or coloured glows. Building interactive states? Layer at least two shadows and animate between them.
One rule that holds across styles: don't use pure black (#000) in your shadows. A slight warm tint (rgba(40, 25, 10, 0.15)) or cool tint (rgba(10, 20, 40, 0.15)) looks far more natural and integrates with your colour palette. Pick a hue from your darkest brand colour and use that as the shadow base.
If you're still calibrating your eye for what looks good, the fastest shortcut is the box shadow generator — tweak values live, see the result on a real card, then copy the exact CSS. No guessing, no save-reload cycles.
FAQ
box-shadow follows the element's rectangular bounding box, even if the element has a transparent cutout. drop-shadow follows the actual rendered pixels, so it works correctly on PNGs with transparency or irregular SVG shapes. For regular elements, box-shadow is faster.
Yes — separate them with commas. They stack in the order listed, with the first one rendered on top. There's no hard browser limit, though 5–6 layers is usually where you hit diminishing visual returns.
No. It's purely visual and doesn't affect the document flow at all. Sibling elements are positioned as if the shadow doesn't exist. Only filter: drop-shadow behaves similarly in that regard.
Use a negative spread-radius to cancel out the sides you don't want. For example, a bottom-only shadow: box-shadow: 0 8px 6px -6px rgba(0,0,0,0.3). The -6px spread pulls the shadow back until only the bottom edge is visible.