CSS drop shadow

filter: drop-shadow() traces the actual pixels, so it works on transparent PNGs, SVG icons and irregular shapes. box-shadow always draws a rectangle.

A CSS drop shadow written as filter: drop-shadow() follows the visible shape of an element, while box-shadow always follows its rectangle.

That is the whole decision. A transparent PNG logo, an SVG icon or a clipped shape needs the filter. A card, a button or a panel does not.

Two identical transparent logos side by side, one with box-shadow, one with drop-shadow.
Two identical transparent logos side by side, one with box-shadow, one with drop-shadow.

Syntax for both

/* follows the shape */
.logo { filter: drop-shadow(0 6px 12px rgba(0, 0, 0, .45)); }

/* follows the box */
.card { box-shadow: 0 6px 12px 0 rgba(0, 0, 0, .45); }

The arguments read the same way at first: horizontal offset, vertical offset, blur radius, colour.

The difference is the fourth length. box-shadow accepts a spread value that grows or shrinks the shadow before blurring. drop-shadow does not.

Feature comparison

filter: drop-shadow() box-shadow
Follows transparency Yes No
Spread value No Yes
Inner shadow (inset) No Yes
Multiple shadows Chain the filter Comma separated
Cost per element Higher Lower
Applies to text too Yes, whole subtree No

The last row surprises people. A filter applies to the element and everything inside it, so text in a filtered container gets a shadow as well.

When only drop-shadow will do

Four cases where box-shadow produces a visibly wrong result:

  1. Transparent PNG. A logo on a transparent background gets a rectangular shadow from box-shadow, outlining the file rather than the mark.
  2. Inline SVG. Icons and illustrations need the shadow to trace the paths. See SVG in HTML for how the markup sits in the page.
  3. Clipped shapes. Anything using clip-path or border-radius: 50% on non-square content.
  4. Text blocks with irregular line lengths, when you want each line shadowed rather than the paragraph box.
An inline SVG icon with filter: drop-shadow applied, shadow tracing the path outline.
An inline SVG icon with filter: drop-shadow applied, shadow tracing the path outline.

Values that look real

One hard shadow reads as a sticker. Stacked soft shadows read as depth.

.card {
  box-shadow:
    0 1px 2px rgba(0, 0, 0, .18),
    0 4px 10px rgba(0, 0, 0, .14),
    0 12px 28px rgba(0, 0, 0, .12);
}

Three rules of thumb:

  • Vertical offset larger than horizontal. Light comes from above in most interfaces, so 0 6px looks natural and 6px 0 looks like a mistake.
  • Blur roughly twice the offset. A 6px drop with a 12px blur sits in the right range for a card.
  • Low opacity, several layers. Three shadows at 0.12 to 0.18 beat one at 0.45.

Chaining the filter version follows the same pattern:

.mark {
  filter:
    drop-shadow(0 1px 1px rgba(0, 0, 0, .2))
    drop-shadow(0 6px 10px rgba(0, 0, 0, .18));
}

Each drop-shadow in the chain runs on the result of the previous one, so the second shadow also shadows the first. Keep chains to two or three.

Shadows on dark backgrounds

Black shadows disappear on dark surfaces. There is nothing darker to shade into.

Two substitutes work. Use a coloured glow keyed to the accent colour, or use a light one-pixel top border to suggest a raised edge.

@media (prefers-color-scheme: dark) {
  .card {
    box-shadow: 0 8px 24px rgba(0, 0, 0, .55);
    border-top: 1px solid rgba(255, 255, 255, .08);
  }
}

A dark theme needs a wider set of adjustments than shadows alone, covered in dark mode CSS.

Inset shadows

Only box-shadow can draw inward. The inset keyword flips the shadow inside the element box.

.well {
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, .35);
  background: #0f172a;
  border-radius: 8px;
}

This is how a pressed button state or a recessed input well is drawn without any extra markup. filter: drop-shadow() has no equivalent, so a shape that needs both an inner and an outer shadow needs both properties.

Shadows as borders

A shadow with zero offset and zero blur is a solid ring, and unlike a border it does not affect layout.

.selected { box-shadow: 0 0 0 2px #60a5fa; }

Nothing shifts when the state changes, because shadows are painted outside the box model. That makes it the standard technique for selection rings and focus indicators on cards in a grid.

Stack it with a real shadow by comma separating, ring first.

.card.selected {
  box-shadow:
    0 0 0 2px #60a5fa,
    0 8px 20px rgba(0, 0, 0, .3);
}

A small elevation scale

Ad hoc shadow values across a document read as inconsistency. Define three levels and reuse them.

Level Use Value
1 Resting cards, table rows 0 1px 2px rgba(0,0,0,.14)
2 Hover, raised panels 0 4px 12px rgba(0,0,0,.16)
3 Menus, dialogs, popovers 0 16px 40px rgba(0,0,0,.24)

Store them as custom properties so the whole document changes in one edit.

:root {
  --e1: 0 1px 2px rgba(0, 0, 0, .14);
  --e2: 0 4px 12px rgba(0, 0, 0, .16);
  --e3: 0 16px 40px rgba(0, 0, 0, .24);
}
.card { box-shadow: var(--e1); }
.card:hover { box-shadow: var(--e2); }

Three levels is usually enough. Beyond that the distinctions stop being visible and the scale becomes decoration rather than information.

Troubleshooting

Symptom Cause
Shadow is a rectangle around a logo box-shadow on a transparent image
Shadow is clipped off An ancestor has overflow: hidden
Text inside also gained a shadow filter applies to the whole subtree
Shadow vanished in dark theme Pure black on a near-black surface
Scrolling stutters Filters on many repeated list items

The overflow case is the most common one reported as a browser bug. The shadow is drawn, then cut by the ancestor.

Checking it before you share the page

Shadows need no external file, so a page using them travels intact.

The shadow test page opened from a share link, shadows rendering as authored.
The shadow test page opened from a share link, shadows rendering as authored.

Open the file in the HTML file opener to confirm it renders outside your project folder. Anything that survives there survives for a reader.

To send it, paste the HTML into a document and turn it into a link. The page keeps its address while you adjust the values, so tuning the shadow does not mean sending a new file.

For the other decorative rules that come up in the same pass, see CSS gradient background and CSS gradient border, Keeping the rules in a style block inside the file is what makes the page travel intact.

Questions people ask

What is the difference between drop-shadow and box-shadow?

drop-shadow() is a filter that follows the visible shape, including transparency in a PNG or the outline of an SVG path. box-shadow follows the element box, so a transparent logo gets a shadow shaped like a rectangle around it.

Why does drop-shadow have no spread value?

The filter takes three lengths at most: x offset, y offset and blur radius. There is no fourth spread argument, unlike box-shadow. To make the shadow larger you either increase the blur or chain a second drop-shadow filter.

Does drop-shadow slow the page down?

It is a filter, so it is computed per pixel and costs more than box-shadow. On a handful of elements this is not measurable. On a long scrolling list of items it can be, so prefer box-shadow when the element is a plain rectangle.

How do I make a shadow look realistic instead of a grey blob?

Stack two or three shadows with different blur radii and low opacity rather than one dark shadow. Keep the vertical offset larger than the horizontal one, since light usually comes from above. Tint the shadow toward the background colour instead of pure black.

Keep reading