Why Shadow an SVG?

Shadows aren't just decorative — they're a functional cue. A persistent shadow can make an icon pop from the canvas, while a shadow toggled on :hover, :focus, or :active gives users real-time feedback. And because real-world light behaves this way, a subtle shadow can make a flat screen feel more natural.

In CSS, box-shadow won't help here: it traces the rectangular bounding box of the element, not the shape of the graphic inside it. That's why — whether you work with inline SVGs or icons set as CSS background images — the usable options boil down to two approaches:

  1. The CSS filter property with drop-shadow()
  2. A dedicated SVG <filter> element

If your icons come from an icon font, the text-shadow property works as a third option. But for the general case, we focus on the two filter approaches.

CSS filter: drop-shadow()

Applying a drop shadow to an SVG shape is a one-liner with the CSS drop-shadow() filter function:

.icon {
  filter: drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));
}

That rule offsets the shadow 3px horizontally, 5px vertically, blurs it by 2px, and colors it at 40% black. The effect traces the icon's rendered edges rather than any rectangular box.

Referencing an SVG filter from CSS

You can also swap the numeric parameters for a reference to an SVG filter that lives in the same document. Define a filter in your HTML:

<svg>
  <filter id="shadow">
    <feDropShadow dx="2" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.5"/>
  </filter>
</svg>

Then call it from CSS by URL:

.icon {
  filter: url(#shadow);
}

The CSS engine finds the filter by its id and applies its definition. This works only when the SVG and the CSS-affected element live in the same HTML document — a CSS url() filter reference can't reach into an external SVG file.

Inside the SVG filter: primitives

SVG filters are containers for one or more filter primitives. Each primitive is a small, single-purpose operation: blur, offset, composite, fill, and so on. They can chain together to produce effects that would be a pain in pure CSS.

The primitive you'll reach for first is <feDropShadow>, which wraps the drop-shadow effect in SVG syntax. It takes the same parameters you saw in CSS, but as attributes:

  • dx — horizontal offset of the shadow
  • dy — vertical offset of the shadow
  • stdDeviation — how much the shadow blurs; supports one or two values, for x and y blur respectively
  • flood-color and flood-opacity — the shadow's color and transparency

This makes the earlier CSS filter equivalent to:

<filter id="drop-shadow">
  <feDropShadow dx="3" dy="5" stdDeviation="2" flood-color="#000" flood-opacity="0.4"/>
</filter>

Building more complex shadows with SVGs

While feDropShadow handles simple cases, an inset shadow or colored glow calls for combining patterns no single primitive covers. This is where SVG filters genuinely compete with image editors. Check the full list of 17 fe-prefixed primitives — the one you need to build an inset shadow requires chaining several in one filter.

An inset-shadow assembly

An SVG <filter> is never rendered directly; it only acts on graphics that reference it. The element itself must live inside an inline SVG, inside a <defs> block, ready for use elsewhere.

The overall flow to create an inset shadow:

<svg>
  <defs>
    <filter id="inset-shadow">
      <feOffset dy="2"/>
      <feGaussianBlur stdDeviation="2" result="offset-blur"/>
      <feComposite operator="out" in="SourceGraphic" in2="offset-blur" result="inverse"/>
      <feFlood flood-color="black" flood-opacity="0.5" result="color"/>
      <feComposite operator="in" in="color" in2="inverse" result="shadow"/>
      <feComposite operator="over" in="shadow" in2="SourceGraphic"/>
    </filter>
  </defs>
  <path ... />
</svg>

Each primitive hands its result to the next one. The in and in2 attributes define which graphic each operation consumes; without explicit result names, the pipeline simply threads through. Here, the shadow needs a version of the icon blurred just on the inside — hence chains like feOffset plus feGaussianBlur, then several composites to clip that blur to the shape's interior.

End with an explanation visual: the original, the blurred, and the combined effect.

For convenience, referencing that assembled filter happens in two syntaxes. The CSS route:

.icon {
  filter: url(#inset-shadow);
}

The SVG attribute route:

<path filter="url(#inset-shadow)" ... />

Both are valid — the first applies it from style rules, the second ties it directly to inline SVG elements.

Comparing the toolkits

Both approaches land right where you'd expect on the trade-off ladder:

  • CSS drop-shadow() is the simpler cut. You write one line and it works. But it's limited — there's no CSS-native way to make an inset shadow or use the full set of filter primitives.
  • SVG <filter> is the full toolbox. 17 primitives give you near-endless effects, from colored shadows to embossed reliefs — but the syntax gets steep. You must also keep the filter definition somewhere in the same HTML document.
  • A pragmatic middle ground: for the common cases, reach for feDropShadow— or equal CSS—first. Only ladder up to composing primitives when a particular shadow needs, say, in-setting or dual-tone treatment.

Browser support is excellent across the board. CSS filter is solid in modern browsers; SVG filters check out even in Internet Explorer and Edge, thanks to the spec's older roots.

These two sources of insight — MDN's guide to working with SVG filter primitives and deeper theoretical dives like Rob O'Leary's Getting Deep Into Shadows — are good for puzzling out composed effects.