Updated November 11, 2026  ·  10 min read  ·  SVGVector

Figma SVG Drop Shadow: Why It Gets Cut Off

A Figma drop shadow survives the SVG export as a <filter>, and three defaults decide whether you ever see it: the filter region, the viewBox, and the fact that blur and offset are user units. Measured on a 100 × 100 shape carrying a shadow of dx 20, dy 20 and σ 8, the default region inked 11,860 pixels while an explicit 200 % region inked 15,418 — 3,558 pixels of shadow that the file describes and the browser never drew.

All fixtures below are 300 × 300 canvases rasterised in Chrome 156 at 1×, drawn through an <img> and read back with getImageData, counting pixels whose alpha exceeds 40/255. The shape is always the same #5850ec rectangle, so the only variable is the filter declaration.

What Figma actually writes

Figma exports a drop shadow as a filter named after the layer, usually filter0_d, containing a chain: feFlood for the colour, feColorMatrix to take the source alpha, feOffset for dx/dy, feGaussianBlur for σ, a feComposite to clip the flood to the blurred alpha, and two feBlend nodes to put the shape back on top. Recent exports also write the region explicitly (x="-50 %" y="-50 %" width="200 %" height="200 %"); older ones and hand-edited ones leave it at the default, which is where most cut-off shadows come from.

The chain is longer than it needs to be. Measured against a single feDropShadow with the same dx, dy, σ and opacity, the two are pixel-identical: 0 differing pixels, 6,610 px of ink each, the same 80 × 83 box. So every fix below can be expressed as one primitive instead of seven.

Cause 1: the filter region is the bounding box plus ten per cent

With no explicit region, a filter operates on a box 10 % larger than the object's bounding box on every side — 120 % in each axis. For a 100 × 100 square that is 110 × 110, so anything the shadow draws past 160 units is discarded before it reaches the canvas.

Region on the filterInkInk boxShadow reaches
default (bbox + 10 %)11,860 px110 × 1109 px past the shape
x="-50 %" y="-50 %" width="200 %" height="200 %"15,418 px128 × 12827 px past the shape

The shadow reaches 27 units past the shape edge because dx is 20 and a σ of 8 spreads roughly another 24 before it fades below the threshold; the default region allows 9. That is 3,558 pixels lost, 23 % of the shadow, and it does not look like a bug: it looks like a harder, shorter shadow, which is why designers often "fix" it by increasing the blur, making the visible part softer and the cut worse.

The fix is one attribute set on the filter, and the numbers have to cover geometry, not the bbox percentage: x and y at least −(dx + 3σ), width and height at least bbox + 2 × (dx + 3σ).

Cause 2: the viewBox cuts what the region allowed

Even with a generous region, the filter output lives inside the SVG viewport. Measured on a 55 × 55 shape at (140, 40) with dx 30, dy 30 and σ 10, inside a 200 × 200 viewBox: 4,288 px of ink, an ink box 60 × 83, and at the rightmost column an alpha of 175 of 255 — a hard edge, not a fade. That 175 is the tell: a shadow that ends naturally reaches 0, a shadow that is clipped ends at whatever value it had when the viewport ran out.

Rendering the identical markup on a canvas padded by 40 units gives 5,767 px of ink, of which 4,322 fall inside the original 200 × 200 window. 1,445 px — a quarter of the shadow — live outside the viewBox and are simply not there.

Reserve the space explicitly: expand the viewBox by dx + |dy| + 3σ on each side, or keep the viewBox and add a transparent rectangle that covers the shadow. The second option is worse, because it makes the file's own bounds lie about its content.

Cause 3: a zero-height bounding box disables the whole filter

This is the one that makes elements vanish. Filters default to filterUnits="objectBoundingBox", which expresses the region as fractions of the object's bounding box. A horizontal <line>, or any path that is perfectly flat, has a bounding box with zero height — a fraction of zero is zero, and the filter is skipped entirely.

Measured: a line from x 20 to 180 at y 100, stroked 6 wide, with a shadow of dy 16 and σ 6, inks 0 pixels. Not a faint shadow — the line itself is gone. Setting filterUnits="userSpaceOnUse" with an explicit x="0" y="0" width="200" height="200" brings back 3,480 px of ink in a 162 × 27 box: the stroke plus its shadow.

Any element whose bounding box is degenerate in one axis needs user-space units, and the list is longer than it looks: horizontal rules, dividers, single-stroke underlines, tables drawn with <line>, and the flat edges of a chart axis.

Cause 4: the shadow scales with the icon, not with the design

dx, dy and stdDeviation are in user units, so they scale with the artwork. The same file rendered at 48 px and at 480 px:

Rendered sizeInk boxShadow height
48 px36 × 3636 px
480 px360 × 360360 px

A ratio of 10.21 for a 10× render, which is exactly what user units mean and exactly what you do not want when a 24 px icon has to work at 240 px as a hero illustration. A shadow tuned at the icon size becomes fog at the hero size. Either keep the viewBox fixed and let CSS scale the whole thing uniformly (accepting the fog), or ship two files with σ recomputed — there is no attribute that pins a blur to screen pixels.

Cause 5: a CSS filter silently replaces the SVG one

If any stylesheet sets the CSS filter property on the same element, the presentation attribute is ignored — the property wins outright, and your carefully exported filter never runs. Measured with a shadow of dx 24:

The second number is just the CSS shadow of the shape; the Figma shadow is gone, along with 2,723 pixels of it. This usually happens by accident, because a "card shadow" utility class gets applied to the wrapper and the SVG is inside it — inheriting nothing, but matching a selector like .card svg { filter: … }.

On the plus side, the two syntaxes are closer than the specification suggests. Measured spread beyond the shape edge: CSS drop-shadow(0 0 8px) gives 16 px, stdDeviation="8" also gives 16 px, and doubling the CSS radius to 16 px gives 32 px. So a conversion is roughly one-to-one at this threshold — but treat it as a visual equivalence, not an arithmetic one, and check the corner softness rather than only the extent.

Cause 6: clips, groups and stroke-only shapes

Three smaller traps, all measured on the same fixture:

Cause 7: a filter chain that eats the artwork

Hand-written or tool-generated chains fail in a way nothing warns you about: the merge order decides whether the shape or its shadow is on top. A chain built as blur → offset → flood → composite, merged as [shadow, blur] instead of [shadow, SourceGraphic], replaces the artwork with its own blur. Measured on the centre pixel: 0,0,0 instead of 88,79,236, with 10,226 px of ink that contains no brand colour at all.

The check is one line: the last feMergeNode in the chain must be SourceGraphic. If it names an intermediate result, you are looking at a shadow pretending to be a shape.

The definition that survives all of it

One filter, user-space units, explicit bounds, and a single primitive. Replace the exported chain with this and keep the four numbers from the Figma panel:

<filter id="shadow" filterUnits="userSpaceOnUse"
        x="-60" y="-60" width="220" height="220">
  <feDropShadow dx="0" dy="4" stdDeviation="4"
                 flood-color="#000000" flood-opacity="0.25"/>
</filter>

The numbers follow from the shadow: 3σ horizontally and vertically plus the offset, so a σ of 4 and a dy of 4 need at least 16 units of padding on every side, and 60 is a comfortable round number for a 24-unit icon. Applying it to the whole file rather than one element is the safer default, because the region then covers every path in the drawing:

const svg = document.querySelector('svg');
const bb = svg.getBBox();
const pad = 60;
let fixed = 0;
svg.querySelectorAll('filter').forEach((f) => {
  if (!f.querySelector('feDropShadow, feGaussianBlur, feOffset')) return;
  f.setAttribute('filterUnits', 'userSpaceOnUse');
  f.setAttribute('x', (bb.x - pad).toFixed(0));
  f.setAttribute('y', (bb.y - pad).toFixed(0));
  f.setAttribute('width', (bb.width + pad * 2).toFixed(0));
  f.setAttribute('height', (bb.height + pad * 2).toFixed(0));
  fixed++;
});
console.log(fixed, 'filters moved to user space');

Measured on a fixture whose horizontal line disappears under a default filter: after the script runs, the line and its shadow ink 3,480 px instead of 0, and the filter carries the four explicit bounds. Running it twice leaves the file byte-identical, because the attributes are set rather than appended — the same build step can be re-run without stacking padding.

A thirty-second check

The one-line rule: an SVG shadow is drawn by a filter whose canvas is bbox + 10 % unless you say otherwise, and whose blur and offset are in user units — so give the filter explicit user-space bounds that cover offset + 3σ, expand the viewBox to match, and never leave a flat element on the default region.

Four measured shadow failures: the default filter region inking 110 by 110 pixels against 128 by 128 with an explicit region; a horizontal line rendering 0 pixels under an objectBoundingBox filter but 3,480 in user-space units; 1,445 pixels of shadow falling outside a tight viewBox with an edge alpha of 175 of 255; and the same shadow growing from 36 to 360 pixels when the icon goes from 48 to 480.
Chrome pixel counts for one shadow declaration: the region, the flat shape that disappears, the viewBox cut, and the scale factor.

Frequently asked questions

Why is my Figma drop shadow cut off on two sides?

Because the filter region defaults to the bounding box plus 10 % per side. Measured on a 100 × 100 shape with dx 20, dy 20 and σ 8: 11,860 px of ink with the default region against 15,418 px with an explicit 200 % region — the shadow stops 9 units out instead of 27, losing 23 % of itself. Set x, y, width and height on the filter.

Why did my line disappear when I added a shadow?

Its bounding box is zero tall, and filter regions default to fractions of that box — a fraction of zero is zero, so the filter is skipped. Measured: 0 px of ink on the line, versus 3,480 px once the filter uses filterUnits="userSpaceOnUse".

Why does the shadow get heavier when I resize the SVG?

Because dx, dy and stdDeviation are user units. The same icon measured a 36 px shadow box at 48 px and a 360 px box at 480 px — a factor of 10.21 for a 10× render. Recompute σ for each size, or accept the softness of the scaled version.

Why did my CSS drop-shadow replace the Figma shadow?

The CSS filter property overrides the filter attribute, so a utility class on the element or an ancestor silently discards the exported filter. Measured: ink drops from 10,077 px to 7,354 px, and the Figma shadow is not drawn at all.

Related SVG Workflows

Shadows are one of six things that break between a Figma canvas and a working SVG. These pages cover the rest, in the order they tend to bite.

Try These Free Online Tools

No download required — convert, compress & optimize SVGs right in your browser

Edit

SVG Editor

Change the filter region and the viewBox, then watch the shadow survive.

Open editor
Validate

SVG Validator

Flag filters with no region, zero-height bboxes and dangling url(#…).

Open validator
Rasterise

SVG to PNG

Render at 2× to see whether the shadow fades out or stops on a hard edge.

Convert to PNG
Clean

SVG Optimiser

Minify a file without collapsing the filter primitives into a no-op.

Open optimiser
Edit

SVG Crop / Flip / Rotate

Expand the viewBox by the shadow padding without moving the art.

Adjust the box
Convert

SVG to Code

Inline the filtered SVG as a component and keep its ids namespaced.

Convert to code