Skip to content
DevDepth

In-depth React internals and CSS layout guides for frontend engineers.

← Back to all articles

In-Depth Article

How CSS Shadows Really Work: `text-shadow`, `box-shadow`, and `drop-shadow()`

Learn when to use `text-shadow`, `box-shadow`, or `drop-shadow()`, how shadows behave with irregular shapes and clipping, and how to avoid common CSS shadow performance traps.

Published: Updated: 8 min readlayout-strategy
css-shadowsbox-shadowdrop-shadowlayout-strategy

Shadows are one of the fastest ways to make UI feel either deliberate or cheap.

Used well, they give a component depth, separation, and a clearer sense of clickability. Used badly, they make cards feel muddy, icons look cut out, and hover states feel heavy instead of responsive.

The frustrating part is that CSS does not have one shadow system. It has several:

  • text-shadow
  • box-shadow
  • filter: drop-shadow()

They look similar at first, but they do not follow the same geometry, do not clip the same way, and are not interchangeable in production.

This guide focuses on the practical questions developers actually hit:

  • when to use each shadow model
  • why box-shadow fails on irregular shapes
  • why inset shadows on media elements often disappoint
  • what clipping and masking do to shadows
  • how to layer shadows without making the UI look dirty
  • how to keep shadow-heavy interfaces from becoming unnecessarily expensive

If you want the broader layout baseline first, start with Modern CSS Layout: History, Axes, Flow, and the Mental Model You Actually Need. If your component also depends on rounded geometry, How border-radius Really Works: Percentages, Nested Corners, Overlap, and Transform is the best companion page.

1. The first decision is not the blur value. It is the shadow model.

Most shadow bugs start before the first pixel is painted.

They start when we pick the wrong mechanism.

Use text-shadow for glyphs and letters

text-shadow follows the rendered text itself.

That makes it the right tool when the thing casting the shadow is the text:

.hero-title {
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.35);
}

It works well for:

  • subtle contrast on text over imagery
  • soft embossed or glowing text treatments
  • layered headline effects

What it does not do is shadow the surrounding box.

Use box-shadow for box geometry

box-shadow follows the element's box model shape, including border-radius.

That makes it the right tool for:

  • cards
  • buttons
  • panels
  • dialogs
  • any component whose shadow should follow the rectangular or rounded box itself
.card {
  border-radius: 1rem;
  box-shadow:
    0 1px 2px rgb(0 0 0 / 0.08),
    0 12px 32px rgb(0 0 0 / 0.14);
}

If the element is fundamentally a box, box-shadow is usually the most honest model.

Use drop-shadow() for the rendered alpha shape

drop-shadow() works on the rendered result of the element rather than on its border box.

That is why it is so useful for:

  • PNGs or SVGs with transparent edges
  • clipped or masked shapes
  • overlapping grouped elements that should cast one combined shadow
  • icons or logos whose visible silhouette matters more than the element's rectangular box
.logo {
  filter: drop-shadow(0 8px 20px rgb(0 0 0 / 0.3));
}

That single difference explains a huge amount of confusion:

box-shadow shadows the box. drop-shadow() shadows the rendered shape.

Once that distinction is clear, most shadow choices get easier.

2. box-shadow and drop-shadow() are similar, but not equivalent

Developers often treat drop-shadow() as if it were a filter-flavored version of box-shadow.

It is not.

They overlap in purpose, but they have different capabilities.

What box-shadow gives you

box-shadow supports:

  • horizontal offset
  • vertical offset
  • blur radius
  • spread radius
  • color
  • inset

That makes it flexible for classic UI elevation and recessed effects.

.field {
  box-shadow:
    inset 0 1px 2px rgb(0 0 0 / 0.12),
    0 0 0 1px rgb(0 0 0 / 0.08);
}

Two especially important features are missing from drop-shadow():

  • spread radius
  • inset shadows

If you need a true inner shadow, box-shadow is the tool. If you need to grow or shrink the shadow shape directly with spread, box-shadow is also the clearer choice.

What drop-shadow() gives you

drop-shadow() follows the visible alpha mask of the rendered result.

That means it can produce a shape-aware shadow around things box-shadow cannot model cleanly, such as:

  • transparent image edges
  • polygons created with clip-path
  • masked avatars or badges
  • grouped overlaps that should read as one silhouette

That is its superpower. Not more parameters. Better shape fidelity.

3. Irregular shapes are where drop-shadow() pulls away

A rounded card and a star-shaped badge do not need the same kind of shadow.

Suppose you clip a badge into a triangle:

.badge {
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}

If you add:

.badge {
  box-shadow: 0 12px 24px rgb(0 0 0 / 0.25);
}

the shadow still belongs to the element's box. It does not tightly follow the triangular silhouette.

Using drop-shadow() is usually the better fit:

.badge {
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
  filter: drop-shadow(0 12px 24px rgb(0 0 0 / 0.25));
}

The same rule applies to PNGs, SVGs, and pseudo-elements with transparent edges. When the visible shape matters more than the box, drop-shadow() usually wins.

4. Overlapping groups often need one shared shadow, not many separate ones

This is another place where box-shadow often disappoints.

Imagine a stack of overlapping chips, avatars, or floating cards. If each child gets its own box-shadow, the result often looks noisy. If only the outer wrapper gets box-shadow, the shadow follows the wrapper rectangle and can leave empty-looking regions.

drop-shadow() can work better when the grouped composition should cast one silhouette-aware shadow:

.chip-group {
  filter: drop-shadow(0 10px 24px rgb(0 0 0 / 0.18));
}

This is especially effective when the children overlap enough that the group should feel like one visual mass instead of several isolated boxes.

There is one caveat worth remembering: filter creates a stacking context. If adding drop-shadow() suddenly changes how the element layers against neighboring UI, that is not random. It is the same class of layering issue explained in Why z-index Is Not Working and How to Fix It.

5. Inset shadows on <img> and <video> are usually a wrapper problem

Many developers try this first:

img {
  box-shadow: inset 0 0 40px rgb(0 0 0 / 0.4);
}

Then the result looks wrong or ineffective.

The practical reason is that media elements such as <img> and <video> behave as replaced elements. In real UI work, that means a direct inset shadow often does not behave like the inset shadow you expect on an ordinary container box.

The safer pattern is to wrap the media and place the shadow or overlay on the wrapper:

<figure class="media-frame">
  <img src="./thumbnail.jpg" alt="Example product card" />
</figure>
.media-frame {
  position: relative;
  overflow: hidden;
  border-radius: 1rem;
}

.media-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 40px rgb(0 0 0 / 0.35);
  pointer-events: none;
}

This is usually a better fit for:

  • vignette effects
  • darker text-legibility overlays
  • image color screens
  • media cards with rounded corners

It also keeps the visual effect separate from the media asset itself, which makes later design changes easier.

6. Shadows do not behave well once a parent starts clipping

Shadows are visual effects outside the core content box, so clipping changes the outcome fast.

There are two practical rules to remember:

  1. An element's own box-shadow can still paint outside its border even if that element uses overflow: hidden.
  2. If an ancestor clips overflow, masking, or clipping paths, shadow pixels outside that clipped region are no longer visible.

That is why this pattern often surprises people:

.shell {
  overflow: hidden;
}

.card {
  box-shadow: 0 16px 40px rgb(0 0 0 / 0.2);
}

If .card sits inside .shell, the shadow can be cut off by the ancestor.

The same idea applies when a parent uses clip-path or mask. Once the pixels outside the visible region are clipped away, the shadow outside that region disappears with them.

When you need a shadow around a clipped or masked shape, a wrapper often solves the problem:

<div class="badge-shadow">
  <div class="badge-shape"></div>
</div>
.badge-shadow {
  filter: drop-shadow(0 10px 24px rgb(0 0 0 / 0.24));
}

.badge-shape {
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}

The wrapper owns the visible shadow. The inner element owns the clipping.

That split is usually more reliable than trying to force one element to both clip itself and cast an intact outer shadow at the same time.

7. Good shadows are usually layered, not just blurrier

A single large blur often looks muddy because real-looking elevation usually has at least two qualities:

  • a tighter shadow near the contact area
  • a softer ambient shadow farther away

That is why layered box-shadow values often look better than one oversized blur:

.panel {
  box-shadow:
    0 1px 2px rgb(0 0 0 / 0.08),
    0 10px 24px rgb(0 0 0 / 0.12),
    0 24px 48px rgb(0 0 0 / 0.08);
}

This kind of stacking gives you more control over:

  • edge sharpness
  • distance
  • softness
  • visual weight

The same idea works for text-shadow too, especially when you need subtle contrast on imagery rather than a cartoon glow.

With drop-shadow(), remember that filter functions are applied in order. Chaining several drop-shadow() calls can create a stronger or more compounded result than developers first expect because each stage works from the already filtered output:

.icon {
  filter:
    drop-shadow(0 1px 1px rgb(0 0 0 / 0.2))
    drop-shadow(0 8px 16px rgb(0 0 0 / 0.14));
}

That can be useful, but it is a reason to stay intentional. More layers do not automatically mean a better shadow.

8. Performance problems usually come from blur, layer count, and animation

Shadows are visually rich because the browser has more work to do.

Three choices matter most:

  • large blur radii
  • many shadow layers
  • animating shadow values directly

The exact cost depends on the browser, device, element size, and the rest of the page, so it is better to think in heuristics than absolutes. But these rules are dependable:

Large blurs get expensive quickly

The larger the blurred area, the more pixels the browser needs to process.

That is why a giant soft halo is usually much more expensive than a small contact shadow.

Many layers add up

A beautifully art-directed ten-layer shadow may be fine on one hero card and overkill on a dense dashboard list.

Use shadow detail where the design payoff is real.

Directly animating box-shadow is often a poor trade

Animating shadow geometry can look nice, but it is often heavier than animating transform or opacity.

One of the most practical production patterns is:

  1. put the richer shadow on a pseudo-element
  2. keep it in place
  3. animate its opacity
.card {
  position: relative;
}

.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 16px 40px rgb(0 0 0 / 0.2);
  opacity: 0;
  transition: opacity 180ms ease;
  pointer-events: none;
}

.card:hover::after {
  opacity: 1;
}

That is not a universal rule, but it is often a cleaner starting point than animating several shadow parameters on the main element itself.

9. A practical shadow choice guide

If you only want one reusable rule from this page, keep this one:

  • use text-shadow when the text itself should cast the shadow
  • use box-shadow when the box is the thing casting the shadow
  • use drop-shadow() when the visible shape is not the same as the box

And for common edge cases:

  • For rounded cards and buttons: start with layered box-shadow
  • For clipped badges, transparent logos, and irregular silhouettes: prefer drop-shadow()
  • For vignette or legibility overlays on media: use a wrapper or pseudo-element
  • For grouped overlaps that should feel like one object: try drop-shadow() on the group
  • For hover elevation: prefer a pseudo-element shadow layer with opacity animation before reaching for animated box-shadow

10. The takeaway

CSS shadows are simple to start and surprisingly easy to misuse.

Most of the confusion disappears once you stop asking only "how much blur should I use?" and start asking a better question:

What shape is actually supposed to cast this shadow?

If the answer is text, use text-shadow.

If the answer is the box, use box-shadow.

If the answer is the rendered silhouette, use drop-shadow().

That one decision will fix more real shadow problems than any collection of preset values ever will.

Publisher and editor

DevDepth Publisher

Independent publisher and frontend engineering writer

DevDepth is maintained as an independent frontend engineering publication focused on React internals, CSS layout systems, and production debugging guides.

React internalsCSS layout systemsfrontend debuggingrendering and performance reasoning

Each article is reviewed before publication for technical accuracy, explanation quality, metadata clarity, and internal-link fit within the current archive.

Last editorial review: Mar 20, 2026