Filters and visual effects
Understand the filter pipeline, use the primitives that earn their cost, and treat filters as the expensive feature they are.
How a filter chain works
<svg viewBox="0 0 240 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Every primitive takes its input from a previous result by name
(or SourceGraphic / SourceAlpha) and writes a named result. -->
<filter id="drop-shadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur"/>
<feOffset in="blur" dx="0" dy="2" result="offset"/>
<feFlood flood-color="#0f172a" flood-opacity="0.35" result="colour"/>
<feComposite in="colour" in2="offset" operator="in" result="shadow"/>
<feMerge>
<feMergeNode in="shadow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<rect x="60" y="35" width="120" height="50" rx="8" fill="#6d28d9" filter="url(#drop-shadow)"/>
</svg>| Primitive | Effect | Cost |
|---|---|---|
feGaussianBlur | Blur | High: scales with the radius and the area |
feOffset | Shift by a distance | Cheap |
feFlood | Fill an area with a colour | Cheap |
feComposite | Combine two inputs with a Porter-Duff operator | Moderate |
feMerge | Layer results together | Cheap |
feColorMatrix | Recolour, saturate, desaturate | Moderate |
feTurbulence | Procedural noise | Very high |
feDisplacementMap | Warp using another image's channels | Very high |
feDropShadow | A blur, an offset and a merge in one | Same as its parts: high |
feComponentTransfer | Per-channel curves | Moderate |
feMorphology | Erode or dilate | High |
⚠️
A filter's default region is
-10% to 110% of the element's bounding box. A blur or a shadow that extends further is clipped, which is the reason a drop shadow looks sliced off. Set x, y, width and height on the filter explicitly — -20% and 140% is a reasonable starting point.The primitives worth knowing
<svg viewBox="0 0 320 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Recolour without touching the source: the matrix is applied per channel. -->
<filter id="sepia">
<feColorMatrix type="matrix" values="
0.393 0.769 0.189 0 0
0.349 0.686 0.168 0 0
0.272 0.534 0.131 0 0
0 0 0 1 0"/>
</filter>
<!-- Desaturate completely -->
<filter id="greyscale">
<feColorMatrix type="saturate" values="0"/>
</filter>
<!-- Push contrast: two linear functions, one per half of the range -->
<filter id="contrast">
<feComponentTransfer>
<feFuncR type="linear" slope="1.8" intercept="-0.4"/>
<feFuncG type="linear" slope="1.8" intercept="-0.4"/>
<feFuncB type="linear" slope="1.8" intercept="-0.4"/>
</feComponentTransfer>
</filter>
<!-- Procedural texture: expensive, but it needs no image asset. -->
<filter id="paper">
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="4" result="noise"/>
<feColorMatrix in="noise" type="saturate" values="0"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.12"/>
</feComponentTransfer>
</filter>
<!-- A wavy displacement driven by turbulence -->
<filter id="wobble">
<feTurbulence type="turbulence" baseFrequency="0.02 0.06" numOctaves="2" result="warp"/>
<feDisplacementMap in="SourceGraphic" in2="warp" scale="12" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<g>
<rect x="20" y="30" width="60" height="60" fill="#0ea5e9" filter="url(#sepia)"/>
<rect x="90" y="30" width="60" height="60" fill="#10b981" filter="url(#greyscale)"/>
<rect x="160" y="30" width="60" height="60" fill="#6d28d9" filter="url(#contrast)"/>
<rect x="230" y="30" width="60" height="60" fill="#f59e0b" filter="url(#wobble)"/>
</g>
</svg>feColorMatrix type="saturate" values="0"is the shortest greyscale filter and the cheapest way to render a disabled state.valuesfor amatrixis a 4x5 grid: four rows for R, G, B and A, five columns for the source channels plus a constant. It is easier to copy a known matrix than to derive one.feTurbulenceis regenerated whenever the filter region changes, so a turbulence filter on a resizing element is a continuous cost. Apply it to a fixed-size element and scale the result.feDisplacementMapreads the red and green channels of its second input by default to decide the x and y offsets. That is why a noise result is a natural driver.- Filters apply to the rendered result, so a filter on a group filters the whole group as one image — which is usually what you want and is also what makes it expensive.
Cost, alternatives and fallbacks
| Want | Filter | Better alternative |
|---|---|---|
| A drop shadow | feDropShadow | filter: drop-shadow() in CSS, or a box-shadow on an HTML element |
| A blur | feGaussianBlur | filter: blur() in CSS, or a pre-blurred image |
| Greyscale | feColorMatrix | filter: grayscale(1) in CSS |
| A texture | feTurbulence | A small tiled raster image |
| A gooey merge | feGaussianBlur plus feColorMatrix | A rendered animation or a sprite sheet |
| A glitch effect | feDisplacementMap | Pre-rendered frames |
| Recolouring an icon | feColorMatrix | currentColor, or a CSS mask |
/* The CSS filter functions cover the common cases with a simpler syntax. */
.shadowed { filter: drop-shadow(0 2px 4px rgba(15, 23, 42, 0.35)); }
.blurred { filter: blur(4px); }
.dimmed { filter: grayscale(1) brightness(0.9); }
.disabled { filter: grayscale(1) opacity(0.5); }
/* Unlike an SVG filter, a CSS drop-shadow follows the alpha shape of the
element, which is exactly what an icon with a transparent background wants.
A box-shadow would draw a rectangle. */
/* Reduce the cost by making the element composited first. */
.animated-with-filter {
will-change: filter, transform;
/* Only worth it when the element genuinely animates: will-change on many
elements is worse than none. */
}
/* A safe fallback for a filter the browser may skip. */
@supports not (filter: blur(4px)) {
.blurred { opacity: 0.6; }
}<!-- If a filter must not recompute on resize, freeze the element's size and
scale the whole thing instead. -->
<svg width="120" height="120" viewBox="0 0 120 120" aria-hidden="true">
<g filter="url(#paper)">
<rect x="0" y="0" width="120" height="120" fill="#f8fafc"/>
</g>
</svg>
<!-- and in CSS scale it, so the filter result is generated once:
.paper-bg { transform: scale(2); transform-origin: 0 0; } -->The honest summary: filters are the most expensive feature in the SVG toolbox and the most commonly overused. Every one of them forces an offscreen render and a composite. If a design can be achieved with a CSS shadow, a pre-rendered asset or a solid colour, that will be faster and more predictable on every device.
FAQ
Why is my drop shadow cut off?
The filter region defaults to 10% of the element's bounding box on each side, which is often too small for the blur and offset. Set
x="-20%" y="-20%" width="140%" height="140%" on the filter, and increase it further for a large blur radius.Why did my animation become slow after adding a filter?
A filter makes the element composite into an offscreen buffer, and if the filtered content changes each frame the buffer is rebuilt each frame. Move the filter to a static parent, pre-render the effect, or use a CSS filter with
will-change on a small element.Related
Transforms, clipping and masking Performance, rendering cost and canvas trade-offs
Last refreshed 2026-09-18.