CSS Shadow Guide: box-shadow vs. drop-shadow vs. text-shadow
A complete guide to the three CSS shadow types: box-shadow, filter: drop-shadow(), and text-shadow. Learn syntax, key differences, when to use each, and live demos.
CSS gives you three distinct ways to add shadows: box-shadow, filter: drop-shadow(), and text-shadow. They look similar at first glance but behave completely differently, have different performance characteristics, and are suited to very different use cases. Choosing the wrong one is one of the most common CSS mistakes in production UIs.
This guide covers all three — with live browser demos, full syntax, the critical differences you need to understand, and the mistakes to avoid.
What is box-shadow and when should you use it?
box-shadow casts a shadow from the element's bounding box — the rectangular region defined by its width, height, and border-radius. It's the right choice for 90% of UI shadow work: cards, buttons, modals, dropdowns, and anything with a clearly rectangular or rounded shape.
What is the full syntax of box-shadow?
box-shadow: offset-x offset-y blur-radius spread-radius color;
box-shadow: inset offset-x offset-y blur-radius spread-radius color;
/* Multiple shadows (first is on top) */
box-shadow:
0 2px 4px rgba(0,0,0,.3),
0 8px 24px rgba(0,0,0,.15); - offset-x — horizontal distance. Positive = right, negative = left.
- offset-y — vertical distance. Positive = down, negative = up.
- blur-radius — how soft the shadow edge is. 0 = hard edge. Can't be negative.
- spread-radius — expands (positive) or shrinks (negative) the shadow before blurring. Unique to
box-shadow—drop-shadow()doesn't have this. - color — any valid CSS color. Use
rgba()orhsl()with alpha for natural-looking shadows. - inset — optional keyword that turns the shadow inward, creating an inner glow or pressed effect.
What is the smooth shadow technique?
A single large-blur shadow (e.g. 0 20px 40px rgba(0,0,0,.4)) looks flat and artificial. Real shadows in nature are denser and sharper near the object, then diffuse as they spread. The smooth shadow technique replicates this by stacking 4–6 layers with progressively larger offsets and decreasing opacity:
box-shadow:
0 1px 2px rgba(0,0,0,.07),
0 2px 4px rgba(0,0,0,.07),
0 4px 8px rgba(0,0,0,.07),
0 8px 16px rgba(0,0,0,.07),
0 16px 32px rgba(0,0,0,.07),
0 32px 64px rgba(0,0,0,.07); The total opacity feels similar, but the shadow has natural depth. This technique was popularised by Philipp Brumm's shadow calculator.
Colored shadows. Replace black with your brand color: rgba(99,102,241,.3) for an indigo glow. This works especially well for buttons and CTAs — the shadow feels like it's emanating from the element itself rather than floating on top of the page.
What is the inset shadow used for?
Adding inset before the offsets pushes the shadow inside the element. Classic uses: pressed button states, recessed input fields, inner glow effects, and neumorphic UI.
/* Pressed button */
box-shadow: inset 0 3px 6px rgba(0,0,0,.4);
/* Top-lit inner glow */
box-shadow:
inset 0 1px 0 rgba(255,255,255,.1),
inset 0 -1px 0 rgba(0,0,0,.3); Try the Box Shadow Generator →
What is filter: drop-shadow() and how is it different?
This is where most developers get confused. filter: drop-shadow() is a CSS filter function — it composites the element into an offscreen buffer and paints the shadow from the element's actual visual outline, not its bounding box.
The practical difference: if you clip an element with clip-path, or use a PNG/SVG with transparent areas, box-shadow still follows the invisible rectangle. drop-shadow() follows the real shape.
(bounding box)
(actual shape)
(misses shape)
(follows shape)
What is the syntax of filter: drop-shadow()?
filter: drop-shadow(offset-x offset-y blur-radius color);
/* Example */
filter: drop-shadow(4px 8px 12px rgba(0,0,0,.5));
/* Chain multiple drop-shadows */
filter:
drop-shadow(0 2px 4px rgba(0,0,0,.3))
drop-shadow(0 8px 16px rgba(0,0,0,.2)); Notice what's missing compared to box-shadow: there is no spread radius and no inset keyword. If you need either of those, use box-shadow.
When should you use drop-shadow() over box-shadow?
- Icons (SVG or PNG with transparency) where the shadow should hug the icon shape
- Images with cutout backgrounds (product photos on white, isolated objects)
- Elements clipped with
clip-pathwhere the shadow should follow the clip - Text rendered as SVG
Performance note. filter: drop-shadow() forces the browser to create a composite layer for the element. On mobile, animating filter is significantly more expensive than animating box-shadow. For hover animations or scroll-driven effects, stick with box-shadow unless you specifically need the shape-following behaviour.
Try the Drop Shadow Generator →
What is text-shadow and what effects can it create?
text-shadow applies a shadow directly to the text glyphs — not to the element box. It's the right property when the subject is the type itself: headlines, display text, hero sections.
What is the syntax of text-shadow?
text-shadow: offset-x offset-y blur-radius color;
/* Multiple shadows */
text-shadow:
0 0 7px #22d3ee,
0 0 20px #22d3ee,
0 0 42px #0ea5e9; text-shadow has no spread radius and no inset keyword. It supports multi-layer stacking with comma separation, same as box-shadow.
How does the neon glow effect work?
The neon technique works by stacking 3 text-shadows at zero offset, each with the same color but increasing blur radius. No offset means the glow radiates evenly. Use the same hue at full saturation for the inner layers, then a slightly darker shade for the outer bloom:
text-shadow:
0 0 4px #fff, /* tight white core */
0 0 11px #fff,
0 0 19px #fff,
0 0 40px #22d3ee, /* cyan outer glow */
0 0 80px #22d3ee,
0 0 90px #22d3ee; Try the Text Shadow Generator →
How do the three shadow types compare?
| Property | box-shadow | filter: drop-shadow() | text-shadow |
|---|---|---|---|
| Follows shape | Bounding box only | Actual visual shape ✓ | Text glyphs ✓ |
| Spread radius | ✓ Yes | ✗ No | ✗ No |
| Inset | ✓ Yes | ✗ No | ✗ No |
| Multi-layer | ✓ Comma-separated | ✓ Chained filters | ✓ Comma-separated |
| Animation cost | Low–medium | High (compositing) | Medium |
| Best for | Cards, buttons, UI elements | SVGs, PNGs, clipped shapes | Headlines, display type |
| Browser support | 99%+ (no prefix) | 96%+ (no prefix) | 99%+ (no prefix) |
What are the most common mistakes with CSS shadows?
Using a single heavy shadow instead of the smooth shadow technique
A single box-shadow: 0 20px 60px rgba(0,0,0,.5) looks like a grey cloud hovering behind the element. It's the most visible sign of amateur CSS. Real shadows are exponential in falloff — dense near the caster, nearly invisible at distance. Three or more layers fixes this permanently.
Animating filter: drop-shadow() on scroll
Scroll-linked filter animations force the browser to composite and repaint every frame. On mid-range Android devices, this is a smooth 60fps experience on desktop and a janky 20fps slideshow on mobile. If you need a shadow on a clipped shape that animates, consider rendering the shape differently (background image, border-radius) so you can use the cheaper box-shadow.
Ignoring opacity in the shadow color
A shadow with solid color (box-shadow: 0 4px 12px #000) looks harsh on any background that isn't pure white. Always use alpha: rgba(0,0,0,.25). Better: use the same hue as your background at higher opacity — a dark navy card on a dark navy page needs a much darker shadow than the same card on white. Shadows that "work" across backgrounds use alpha-tuned colors, not black.
Using box-shadow on a clip-path element expecting it to follow the clip
This is the most confusing mismatch. clip-path clips the element — including its box-shadow. If you add a box-shadow to a clipped element, the shadow is clipped too and mostly or entirely disappears. The fix: wrap the clipped element in a parent and apply filter: drop-shadow() to the parent instead.
/* Wrong — shadow gets clipped */
.shape {
clip-path: polygon(50% 0, 100% 100%, 0 100%);
box-shadow: 0 8px 20px rgba(0,0,0,.5); /* clipped, mostly invisible */
}
/* Correct — parent carries the drop-shadow */
.shape-wrapper {
filter: drop-shadow(0 8px 14px rgba(0,0,0,.5));
}
.shape-wrapper .shape {
clip-path: polygon(50% 0, 100% 100%, 0 100%);
} (shadow disappears)
(correct)
Forgetting that text-shadow doesn't respect overflow: hidden
A text-shadow with a large blur radius can visually overflow its container even when overflow: hidden is applied to the parent. If your layout has tight boundaries and a glowing headline, test at small viewport sizes — the glow may bleed past the container edge.
Quick-reference: which shadow property should you reach for?
- UI card, button, modal, dropdown →
box-shadow - Hover lift on a card →
box-shadowtransition (cheap, composited by GPU) - Inner glow, pressed state, recessed input →
box-shadow: inset - SVG icon, PNG cutout, clipped shape →
filter: drop-shadow()on parent - Headline, display text, neon effect →
text-shadow - Glowing button that also needs shape hug →
box-shadowfor the button rectangle +filter: drop-shadow()only if the shape demands it