The Complete CSS Shadow Guide: box-shadow, drop-shadow, and text-shadow explained
Master all three CSS shadow techniques — syntax, parameters, rendering differences, real-world patterns, and performance implications.
Shadows are one of the simplest tools in CSS, yet most tutorials only scratch the surface. This guide covers all three shadow mechanisms in depth — their syntax, parameters, rendering differences, real-world use cases, and performance implications — so you can reach for the right technique every time.
Why does CSS have three shadow properties?
Each shadow mechanism exists to solve a different problem. They emerged at different points in CSS history and target fundamentally different rendering targets:
box-shadow— shadows for rectangular boxes (the CSS box model). Available since CSS 2.1, standardized in CSS3.filter: drop-shadow()— shadows that follow the actual visible shape of an element, including transparency. Part of the CSS Filter Effects Level 1 spec.text-shadow— shadows applied directly to glyph outlines. Technically the oldest of the three, introduced in CSS 2, briefly dropped, then re-standardized in CSS3.
The confusion arises because all three produce what the human eye perceives as "a shadow", but the rendering engine handles them at completely different stages of the painting pipeline.
Use box-shadow for UI elements with solid backgrounds. Use drop-shadow for PNGs, SVGs, or anything with transparency. Use text-shadow exclusively on text.
box-shadow: the workhorse
It draws one or more shadows behind (or inside) an element's border box — the most versatile and widely used shadow property.
Syntax
box-shadow: [inset] offset-x offset-y [blur-radius] [spread-radius] color; Each value does exactly one job:
| Parameter | Default | What it controls |
|---|---|---|
inset | omitted (outer) | Draws the shadow inside the element instead of behind it |
offset-x | required | Horizontal distance. Positive = right, negative = left |
offset-y | required | Vertical distance. Positive = down, negative = up |
blur-radius | 0 | Gaussian blur amount. Higher = softer, more spread out |
spread-radius | 0 | Expands (+) or contracts (–) the shadow before blurring |
color | currentColor | Shadow color — always use rgba() or hsla() for realism |
The spread radius: the underused parameter
Most developers use offset + blur and stop there. The spread radius unlocks a different class of effects entirely. At spread: 0 and blur: 0 it becomes a perfect, stackable CSS border that doesn't affect layout:
/* Layered outline rings — no layout impact */
box-shadow:
0 0 0 4px rgba(201,184,154,.4),
0 0 0 8px rgba(201,184,154,.1); With a negative spread, you can trim a blurry shadow into a directional one that only appears below an element:
/* Shadow only at the bottom — more realistic */
box-shadow: 0 12px 24px -8px rgba(0,0,0,.6); Live examples
4px 4px 8px rgba(0,0,0,.7) 0 0 0 4px accent inset 0 4px 12px 0 2px 4px + 0 8px 24px + ring 0 0 24px + 0 0 48px light + dark inset pair backdrop-blur + shadow Multiple shadows: order matters
You can stack an unlimited number of shadows with a comma-separated list. They render front-to-back — the first shadow in the list sits on top. This lets you layer ambient + directional shadows for depth that reads naturally in both light and dark modes.
/* Three-layer system used in serious design systems */
box-shadow:
0 1px 2px rgba(0,0,0,.3), /* close shadow: sharp, small */
0 4px 16px rgba(0,0,0,.25), /* mid shadow: soft ambient */
0 0 0 1px rgba(255,255,255,.05); /* subtle highlight ring */ Inset shadows: depth and pressed states
The inset keyword moves the shadow inside the element's border edge. The offset direction flips intuitively: a positive Y offset places the shadow along the top inner edge, as if light is coming from above. This is essential for pressed button states, embossed text inputs, and neumorphism:
/* Input focus state — depth without a visible border */
input:focus {
box-shadow:
inset 0 2px 6px rgba(0,0,0,.5),
0 0 0 2px rgba(201,184,154,.4);
} Free Tool Box Shadow Generator Tweak all six parameters visually and copy production-ready CSS filter: drop-shadow() — shape-aware shadows
filter: drop-shadow() is not a property — it is a CSS filter function. Filters are applied after painting, operating on the final rendered pixels of an element and its descendants. This distinction has two major consequences:
- The shadow follows the actual visible outline, respecting alpha transparency in PNGs and the shape of SVG paths.
- The filter affects the entire subtree, including children. This means it works on groups of elements automatically.
Syntax
filter: drop-shadow(offset-x offset-y blur-radius color); Notice: no spread-radius and no inset keyword. The spec deliberately omitted them. If you need spread on a shape-aware shadow, stack two drop-shadow() calls with different blur radii instead.
Where box-shadow fails and drop-shadow shines
Imagine a transparent PNG logo on a coloured page background. Apply box-shadow and you get a rectangle shadow around the invisible bounding box — the shadow bleeds into the transparent areas. Apply drop-shadow() and the shadow hugs the actual visible logo shape.
The same logic applies to:
- SVG elements — paths, polygons, compound shapes
- CSS clip-path — the shadow follows the clip boundary
- Rotated or transformed elements — the shadow correctly follows the post-transform shape
- Component groups — one filter on a parent shadows all children as a unit
/* SVG icon with proper shadow */
.icon-wrapper {
filter: drop-shadow(0 4px 8px rgba(0,0,0,.6));
}
/* PNG logo — shadow follows logo shape, not its bounding box */
.logo {
filter: drop-shadow(2px 4px 12px rgba(0,0,0,.5));
}
/* Stacked for a glow effect on an irregular shape */
.neon-svg {
filter:
drop-shadow(0 0 6px rgba(201,184,154,.8))
drop-shadow(0 0 20px rgba(201,184,154,.4));
} The key trade-off: stacking context
Applying filter creates a new stacking context and a new compositing layer. This is usually fine, but it means the element can no longer bleed over other compositing layers correctly — which occasionally causes z-index issues. If you encounter that, consider whether box-shadow can approximate the effect instead.
text-shadow: typography depth
text-shadow is applied exclusively to the glyph outlines of text — not the element's box. It paints at the same stage as the text itself, behind the glyphs, making it the right choice whenever you want depth or legibility on typographic elements.
Syntax
text-shadow: offset-x offset-y [blur-radius] color; Compared to box-shadow, there is no spread-radius and no inset. Multiple shadows are comma-separated, with the same front-to-back ordering.
Live examples
2px 2px 4px rgba(0,0,0,.8) 0 0 10px + 0 0 30px light + dark offset pair hard double offset Common use cases
Legibility over images: A subtle text-shadow: 0 1px 3px rgba(0,0,0,.6) makes white text readable over bright photography without needing a dark overlay.
Neon/glow typography: Stack two zero-offset shadows with increasing blur radii. The inner shadow creates the intense core, the outer creates the atmospheric bloom:
.neon {
color: #c9b89a;
text-shadow:
0 0 8px rgba(201,184,154,.9),
0 0 24px rgba(201,184,154,.5),
0 0 60px rgba(201,184,154,.2);
} Letterpress / emboss: Combine a light shadow offset up-left with a dark shadow offset down-right. The direction determines whether the text appears raised or recessed:
/* Raised (light = up-left, dark = down-right) */
text-shadow: -1px -1px 0 rgba(255,255,255,.15), 1px 1px 0 rgba(0,0,0,.6);
/* Recessed (reversed) */
text-shadow: 1px 1px 0 rgba(255,255,255,.1), -1px -1px 0 rgba(0,0,0,.5); Hard drop shadow (retro/comic style): Zero blur, larger offset, stacked hard shadows:
.retro {
text-shadow: 3px 3px 0 #7a5c38, 6px 6px 0 rgba(0,0,0,.25);
} Free Tool Text Shadow Generator Design and export text shadow effects with live preview Side-by-side comparison
| Feature | box-shadow | drop-shadow() | text-shadow |
|---|---|---|---|
| Targets | Element box model | Rendered pixel shape | Text glyph outlines |
| Respects transparency | No — ignores alpha | Yes — shape-aware | N/A (text only) |
| spread-radius | Yes | No | No |
| inset keyword | Yes | No | No |
| Multiple layers | Yes, comma-separated | Yes, chained functions | Yes, comma-separated |
| Stacking context | No new context | Creates new context | No new context |
| Applies to children | No | Yes (entire subtree) | No |
| Works on SVG shapes | Box only | Yes, per-path | N/A |
| CSS animation | Animatable | Animatable | Animatable |
| GPU composited | Often, on promoted layers | Yes, always | Rarely |
The one question that determines which to use
Ask yourself: does the element have a rectangular, opaque background? If yes, use box-shadow — it is faster to reason about and has no stacking context side effects. If no (transparent PNG, SVG, irregular shape, clip-path), use drop-shadow(). If your target is text, use text-shadow.
Production recipes and patterns
Elevation system (Material-style)
Rather than hardcoding shadow values throughout a codebase, define an elevation scale as CSS custom properties. This makes global depth consistent and easy to update:
:root {
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
--shadow-2: 0 2px 6px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.3);
--shadow-3: 0 4px 16px rgba(0,0,0,.3), 0 2px 4px rgba(0,0,0,.3);
--shadow-4: 0 8px 32px rgba(0,0,0,.28), 0 4px 8px rgba(0,0,0,.25);
--shadow-5: 0 16px 48px rgba(0,0,0,.25), 0 8px 16px rgba(0,0,0,.2);
}
.card { box-shadow: var(--shadow-2); }
.card:hover { box-shadow: var(--shadow-4); transition: box-shadow .2s ease; }
.modal { box-shadow: var(--shadow-5); } Colored shadows
One of the most underused techniques: instead of black shadows, tint the shadow with the element's dominant color. This reads more naturally in both light and dark themes and creates a glow-like quality without looking harsh:
/* Button with colored shadow matching brand color */
.btn-primary {
background: #c9b89a;
box-shadow: 0 4px 20px rgba(201,184,154,.4), 0 2px 6px rgba(201,184,154,.3);
}
.btn-primary:hover {
box-shadow: 0 6px 28px rgba(201,184,154,.55), 0 3px 8px rgba(201,184,154,.4);
} Glassmorphism with shadow
Glassmorphism requires a carefully calibrated interplay of backdrop-filter, a translucent background, a semi-transparent border, and box-shadow to separate the card from the background:
.glass-card {
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(16px) saturate(1.2);
-webkit-backdrop-filter: blur(16px) saturate(1.2);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
box-shadow:
0 8px 32px rgba(0,0,0,.4), /* ambient depth */
inset 0 1px 0 rgba(255,255,255,.08); /* top highlight */
} PixCode Glassmorphism Generator exports the complete CSS including vendor prefixes.
Focus rings with box-shadow
Replacing the default browser outline with box-shadow gives you full control over style while maintaining accessibility. The trick is using a spread-only shadow at 0 blur:
:focus-visible {
outline: none;
box-shadow:
0 0 0 2px var(--bg), /* gap between element and ring */
0 0 0 4px rgba(201,184,154,.7); /* visible ring */
} Neumorphism
Neumorphism uses paired inset and outer shadows in light and dark variants derived from the background color. The element, shadow, and background must all use closely related tones — extreme contrast breaks the illusion:
/* Element bg = slightly lighter than page bg */
.neumorphic {
background: #1c1c1c;
border-radius: 16px;
box-shadow:
8px 8px 16px #0a0a0a, /* dark shadow: down-right */
-8px -8px 16px #2e2e2e; /* light shadow: up-left */
}
.neumorphic.pressed {
box-shadow:
inset 4px 4px 10px #0a0a0a,
inset -4px -4px 10px #2e2e2e;
} Performance considerations
Shadows are one of the rendering operations developers worry about most, often without cause. Here is what actually happens:
✓ Fast paths
box-shadow on GPU-composited layers (position:fixed, will-change:transform). filter: drop-shadow() is always GPU-accelerated. Animating box-shadow via CSS transitions on promoted elements.
✗ Slow paths
Animating box-shadow on un-promoted elements triggers layout + paint on every frame. Large blur radii on filter: drop-shadow() are expensive. text-shadow with very large blur on many elements.
When animating shadows
The safest pattern for animated shadows (hover elevations, loading states) is to promote the element first, then transition the shadow:
.card {
will-change: transform; /* promote to compositing layer */
transform: translateZ(0); /* trigger layer in older browsers */
transition: box-shadow .2s ease, transform .2s ease;
box-shadow: var(--shadow-2);
}
.card:hover {
transform: translateY(-2px); /* move on GPU — free */
box-shadow: var(--shadow-4); /* re-composite — also fast */
} Don't blanket-apply will-change — each promoted layer consumes GPU memory. Use it only on elements that will actually animate, and remove it once the animation ends if added via JavaScript.
blur-radius vs spread-radius performance
Increasing blur-radius is more expensive than increasing spread-radius, because blur requires a Gaussian convolution that scales with the pixel area. A spread: 10px, blur: 2px shadow is cheaper than spread: 0, blur: 12px for the same visual footprint.
Interactive shadow generators on PixCode.io
All three shadow techniques have dedicated interactive generators on PixCode — tweak parameters with sliders, see the live result, and copy production-ready CSS with one click.