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.

~15 min read Feb 2025

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.

⚡ Rule of thumb

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
insetomitted (outer)Draws the shadow inside the element instead of behind it
offset-xrequiredHorizontal distance. Positive = right, negative = left
offset-yrequiredVertical distance. Positive = down, negative = up
blur-radius0Gaussian blur amount. Higher = softer, more spread out
spread-radius0Expands (+) or contracts (–) the shadow before blurring
colorcurrentColorShadow 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.

Free Tool CSS Filter Generator Combine drop-shadow with blur, brightness, and saturate — live preview

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

Heading
2px 2px 4px rgba(0,0,0,.8)
Neon
0 0 10px + 0 0 30px
Emboss
light + dark offset pair
Retro
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

Featurebox-shadowdrop-shadow()text-shadow
TargetsElement box modelRendered pixel shapeText glyph outlines
Respects transparencyNo — ignores alphaYes — shape-awareN/A (text only)
spread-radiusYesNoNo
inset keywordYesNoNo
Multiple layersYes, comma-separatedYes, chained functionsYes, comma-separated
Stacking contextNo new contextCreates new contextNo new context
Applies to childrenNoYes (entire subtree)No
Works on SVG shapesBox onlyYes, per-pathN/A
CSS animationAnimatableAnimatableAnimatable
GPU compositedOften, on promoted layersYes, alwaysRarely

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 */
}
⚠️ Caution

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.

Frequently Asked Questions

What is CSS box-shadow? +
CSS box-shadow adds a rectangular shadow behind an element using the syntax: offset-x offset-y blur-radius spread-radius color. You can stack multiple shadows with commas and use the inset keyword to render the shadow inside the element.
How does filter drop-shadow differ from box-shadow? +
box-shadow follows the rectangular bounding box of an element and ignores transparent pixels. filter drop-shadow traces the actual visible shape — essential for PNGs with transparency, SVGs and irregular shapes where box-shadow would look rectangular.
Can you animate CSS shadows without hurting performance? +
Yes, but carefully. Animating box-shadow triggers layout and paint on non-composited elements. The safest pattern is to animate opacity between two pseudo-elements holding the shadows, or to use transform on an already-composited element which stays on the GPU.
What is the spread-radius parameter in box-shadow? +
Spread-radius is the fourth value in box-shadow. A positive value expands the shadow beyond the blur area; a negative value shrinks it. A spread of 0 means the shadow is exactly as large as the element before blur is applied.
When should I use text-shadow instead of drop-shadow? +
Use text-shadow for typographic effects — legibility boosts, glow effects, embossed text and retro double-offset styles. Use filter drop-shadow on elements that need the shadow to follow a non-rectangular boundary, like an SVG speech bubble or a logo with transparency.
How do I create a neumorphic card with box-shadow? +
Neumorphism requires two shadows: one lighter than the background (top-left) and one darker (bottom-right). The element and page must share nearly the same background color. Example: box-shadow: 8px 8px 16px #0a0a0a, -8px -8px 16px #2e2e2e on a #1c1c1c card over a #0e0e0e background.
What browsers support CSS box-shadow and CSS filter? +
box-shadow is supported in all browsers since IE 9 (2011). filter drop-shadow is supported in all modern browsers since 2013. Internet Explorer does not support filter — use box-shadow as a fallback for IE if needed.