← Back to Index

Lesson 7

SVG Filters

Filters are SVG's pixel-manipulation layer. Everything covered so far - shapes, paths, gradients, clips, masks - operates on geometry and paint. Filters operate on the rendered pixels after all that geometry and paint has been rasterised. They let you blur, distort, recolour, composite, and light your graphics in ways that geometry alone can't achieve.

1. The Filter Model

A filter is a pipeline of one or more filter primitives - small, single-purpose image operations chained together. Each primitive takes one or two inputs, processes them, and produces an output. You wire these primitives together by naming their inputs and outputs.

Filters are raster operations on vector sources

The browser first renders your SVG element to pixels (rasterises it), then feeds those pixels through the filter pipeline. The output is a new pixel buffer that replaces the original rendering. This means filters are resolution-dependent - they operate on actual screen pixels, not on the abstract geometry. That's why blurs look different at different zoom levels (unlike clipping, which is always crisp).

Basic structure

<svg viewBox="0 0 300 200">
  <defs>
    <filter id="my-filter">
      <!-- Filter primitives go here -->
      <feGaussianBlur stdDeviation="3"/>
    </filter>
  </defs>

  <!-- Apply the filter -->
  <rect width="200" height="100" fill="coral"
        filter="url(#my-filter)"/>
</svg>
No filter feGaussianBlur (3)

Left: unfiltered rect. Right: same rect with a 3px Gaussian blur applied.

The filter region

Every filter has a filter region - the rectangular area in which the filter operates. By default, it extends 10% beyond the element's bounding box in each direction (same as masks):

<filter id="my-filter" x="-10%" y="-10%" width="120%" height="120%">
  <!-- primitives here -->
</filter>

These four values define a rectangle, not a symmetric padding. The x/y set where the region starts (relative to the bounding box origin), and width/height set how far it extends. The right edge is at x + width = -10% + 120% = 110%, i.e. 10% beyond the right edge of the bounding box. Same logic vertically. The result is 10% extra on all four sides - but you can make it asymmetric if needed (e.g. more room below for a downward shadow):

<!-- Asymmetric: 5% extra left/right/top, 25% extra at the bottom -->
<filter x="-5%" y="-5%" width="110%" height="130%">
  <feDropShadow dx="0" dy="10" stdDeviation="4"/>
</filter>

This padding is necessary because many filter effects (like blur) expand the visual footprint of an element. If the filter region were exactly the bounding box, blurred edges would get clipped. You can override these values when you need more (or less) space.

Gotcha: filter output clipped unexpectedly?

If your filter produces output larger than the default 120% region (e.g. a very large blur, or a displacement that moves pixels far from the source), increase the filter region: <filter x="-25%" y="-25%" width="150%" height="150%">. A common debugging step when a filter looks "cut off" at the edges.

<filter> element attributes

AttributeValuesDescription
idstringUnique identifier for referencing via filter="url(#id)"
xlength (default -10%)Left edge of filter region
ylength (default -10%)Top edge of filter region
widthlength (default 120%)Width of filter region
heightlength (default 120%)Height of filter region
filterUnitsobjectBoundingBox | userSpaceOnUseCoordinate system for x/y/width/height (default: objectBoundingBox)
primitiveUnitsuserSpaceOnUse | objectBoundingBoxCoordinate system for primitive attributes like stdDeviation (default: userSpaceOnUse)

Inputs and outputs: the wiring model

Each filter primitive can take named inputs (in) and produce named outputs (result). This is how you chain primitives into a pipeline:

<filter id="drop-shadow">
  <!-- Step 1: blur the source graphic -->
  <feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blurred"/>

  <!-- Step 2: offset the blurred result -->
  <feOffset in="blurred" dx="3" dy="3" result="shadow"/>

  <!-- Step 3: composite the original on top of the shadow -->
  <feMerge>
    <feMergeNode in="shadow"/>
    <feMergeNode in="SourceGraphic"/>
  </feMerge>
</filter>
SourceAlpha feGaussianBlur feOffset feMerge Output

Built-in input keywords (implicit - you never define these)

When a filter runs, the browser automatically creates these named images and makes them available as inputs. They're reserved names, not something you declare:

Default wiring: implicit chaining

If you omit in, a primitive uses the output of the previous primitive. If it's the first primitive, it uses SourceGraphic. If you omit result, the output is unnamed but still available as the implicit input to the next primitive. This means simple linear chains don't need explicit in/result attributes at all:

<!-- Explicit wiring (verbose but clear) -->
<filter id="a">
  <feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blurred"/>
  <feOffset in="blurred" dx="3" dy="3" result="shadow"/>
</filter>

<!-- Implicit wiring (identical behaviour) -->
<filter id="b">
  <feGaussianBlur in="SourceAlpha" stdDeviation="4"/>
  <feOffset dx="3" dy="3"/>
</filter>

You only need explicit in/result names when the pipeline branches - when a primitive needs to reference something other than "the previous output." Without names, implicit chaining gives you a straight line (A→B→C). But real filter pipelines often aren't straight lines:

SourceAlpha → feGaussianBlur → feOffset → ┐
                                            ├→ feMerge → output
SourceGraphic ─────────────────────────────┘

Here feMerge needs two inputs: the offset blur (the thing above it) and the original SourceGraphic (which is not "the previous output" - it's a completely different stream). Without explicit names, there's no way to express this.

Concretely, you need in/result when:

Case 1: Reaching back (not "the one above me")

The drop shadow: feMerge needs the original SourceGraphic, which isn't the previous output - the previous output is the offset shadow.

<filter id="shadow">
  <feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/>
  <feOffset in="blur" dx="3" dy="3" result="shadow"/>
  <feMerge>
    <feMergeNode in="shadow"/>        <!-- previous output ✓ -->
    <feMergeNode in="SourceGraphic"/>  <!-- reaches BACK to the original -->
  </feMerge>
</filter>
SourceAlpha → feGaussianBlur → feOffset ──→ ┐
                                             ├→ feMerge → output
SourceGraphic ───────────────────────────────┘
                              (reaches back)

Case 2: Fan-out (two primitives consume the same result)

Suppose you want both a blurred shadow and a colour-shifted copy from the same blur operation:

<filter id="fan-out">
  <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blurred"/>

  <!-- Branch A: offset the blur -->
  <feOffset in="blurred" dx="4" dy="4" result="shadow"/>

  <!-- Branch B: hue-rotate the SAME blur -->
  <feColorMatrix in="blurred" type="hueRotate" values="90" result="tinted"/>

  <!-- Merge both branches + original -->
  <feMerge>
    <feMergeNode in="shadow"/>
    <feMergeNode in="tinted"/>
    <feMergeNode in="SourceGraphic"/>
  </feMerge>
</filter>
                        ┌→ feOffset ────────→ ┐
SourceGraphic → feBlur ─┤                      ├→ feMerge → output
                        └→ feColorMatrix ──────┤
SourceGraphic ─────────────────────────────────┘
                (fan-out: both read "blurred")

Case 3: Two-input primitives (in + in2)

Primitives like feComposite, feBlend, and feDisplacementMap require two explicit inputs. Implicit chaining can only provide one (the previous output), so you must name the other:

<filter id="two-inputs">
  <!-- Generate noise -->
  <feTurbulence baseFrequency="0.03" result="noise"/>

  <!-- Displace SourceGraphic USING the noise -->
  <feDisplacementMap in="SourceGraphic" in2="noise"
    scale="15" xChannelSelector="R" yChannelSelector="G"/>
</filter>
feTurbulence ────────→ (noise) ─────────→ in2 ┐
                                               ├→ feDisplacementMap → output
SourceGraphic ─────────────────────────→ in   ┘
          (two separate streams into one primitive)
Rule of thumb

If your pipeline is a straight line (A→B→C), you don't need in/result. The moment it branches, merges, or a primitive needs two inputs, you need explicit names to tell the browser which wire connects where.

2. The 17 Filter Primitives

SVG defines exactly 17 filter primitives. Each does one specific operation. Here's the complete set, grouped by function:

CategoryPrimitiveWhat it does
Blur & ShadowfeGaussianBlurGaussian blur with configurable radius
feDropShadowShorthand: offset + blur + colour (SVG 2)
ColourfeColorMatrix4×5 matrix colour transform (hue rotate, saturate, etc.)
feComponentTransferPer-channel transfer functions (gamma, threshold, etc.)
feFloodFills the filter region with a solid colour
CompositingfeCompositePorter-Duff compositing (in, out, atop, xor, over, arithmetic)
feBlendBlend modes (normal, multiply, screen, darken, lighten)
feMergeLayer multiple inputs on top of each other
SpatialfeOffsetShifts pixels by dx/dy (used in shadow pipelines)
feTileTiles the input to fill the filter region
feDisplacementMapDistorts pixels using another image as a displacement map
MorphologyfeMorphologyErode or dilate (shrink/expand) shapes
feConvolveMatrixCustom convolution kernel (sharpen, emboss, edge-detect)
LightingfeDiffuseLightingMatte surface lighting (Lambertian reflection)
feSpecularLightingShiny surface lighting (specular highlights)
TexturefeTurbulenceGenerates Perlin noise or turbulence patterns
UtilityfeImageLoads an external image or references an SVG element as filter input

We'll cover the most important ones in depth. The rest follow the same pattern - once you understand the wiring model and a few core primitives, the others are straightforward to pick up from the reference doc.

3. feGaussianBlur

The workhorse filter primitive. It blurs the input by a specified radius using a Gaussian (bell curve) distribution. The stdDeviation attribute controls the blur radius - larger values = more blur. It accepts one or two values:

<filter id="blur-example">
  <!-- Single value: equal blur in x and y -->
  <feGaussianBlur stdDeviation="5"/>

  <!-- Two values: different blur for x and y (motion blur effect) -->
  <feGaussianBlur stdDeviation="8 0"/>
</filter>
stdDeviation="2" stdDeviation="5" stdDeviation="10" stdDeviation="8 0" (horizontal only)

Increasing blur radius. The last example uses two values (x-blur, y-blur) to create a directional motion blur.

stdDeviation is in filter coordinate units, not pixels

The blur radius is measured in the filter's coordinate system (which by default matches the user coordinate system of the filtered element). If you're working inside a viewBox, the blur amount relates to those units. When the SVG is scaled up on a high-DPI screen, the browser applies more actual pixels of blur to achieve the same visual radius - so the blur always looks proportionally correct.

Attributes

AttributeValuesDescription
inSourceGraphic | SourceAlpha | result nameInput image
stdDeviationnumber | "x y"Blur radius. Single value = equal x/y. Two values = independent horizontal/vertical blur.
edgeModeduplicate | wrap | noneHow to handle pixels at the edge of the input (default: none = transparent black)

4. Building a Drop Shadow (The Classic Pipeline)

Drop shadows are the "hello world" of filter pipelines. They combine three primitives in sequence, and understanding this chain teaches you 80% of what you need to know about filter wiring:

  1. feGaussianBlur on SourceAlpha - creates a blurred silhouette
  2. feOffset - shifts the blurred silhouette down and to the right
  3. feMerge - layers the original graphic on top of the shadow
<filter id="drop-shadow">
  <!-- 1. Blur the alpha channel (silhouette) -->
  <feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/>

  <!-- 2. Offset the blurred shadow -->
  <feOffset in="blur" dx="4" dy="4" result="shadow"/>

  <!-- 3. Layer: shadow behind, original on top -->
  <feMerge>
    <feMergeNode in="shadow"/>
    <feMergeNode in="SourceGraphic"/>
  </feMerge>
</filter>

Manual drop shadow: blur SourceAlpha → offset → merge with SourceGraphic on top.

Why SourceAlpha and not SourceGraphic?

If you blur SourceGraphic, the shadow retains the element's colours - you'd get a coloured halo rather than a dark shadow. SourceAlpha gives you a black silhouette (all the alpha but no colour), which is what shadows look like in the real world.

Adding colour to the shadow

The manual pipeline above produces a black shadow. To colour it, insert an feFlood + feComposite step:

<filter id="coloured-shadow">
  <feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/>
  <feOffset in="blur" dx="4" dy="4" result="offsetBlur"/>

  <!-- Flood with desired shadow colour -->
  <feFlood flood-color="rgba(97, 175, 239, 0.5)" result="colour"/>

  <!-- Clip the flood to the shadow shape -->
  <feComposite in="colour" in2="offsetBlur" operator="in" result="shadow"/>

  <feMerge>
    <feMergeNode in="shadow"/>
    <feMergeNode in="SourceGraphic"/>
  </feMerge>
</filter>
Filters!

Coloured drop shadow using feFlood + feComposite to tint the shadow blue.

5. feDropShadow (The Shorthand)

SVG 2 introduced feDropShadow as a convenience primitive that combines the blur + offset + coloured shadow into a single element. It does the same thing as the manual pipeline above, but in one line:

<filter id="easy-shadow">
  <feDropShadow dx="3" dy="4" stdDeviation="4"
                flood-color="rgba(0,0,0,0.4)"/>
</filter>
feDropShadow

feDropShadow - one element does what the manual pipeline needed five.

feDropShadow automatically merges the original on top

Unlike the manual pipeline, you don't need an feMerge step - feDropShadow implicitly composites the source graphic over the shadow. Its output is the complete "element + shadow" image.

Browser support

feDropShadow is well-supported in all modern browsers (Chrome 80+, Firefox 72+, Safari 13.1+). If you need to support older browsers, use the manual pipeline from Section 4.

Attributes

AttributeValuesDescription
inSourceGraphic | SourceAlpha | result nameInput image
dxnumber (default 2)Horizontal shadow offset
dynumber (default 2)Vertical shadow offset
stdDeviationnumber | "x y" (default 2)Blur radius for the shadow
flood-colorcolour (default black)Shadow colour
flood-opacity0–1 (default 1)Shadow opacity

6. feColorMatrix

This is SVG's colour Swiss Army knife. It applies a 4×5 matrix transformation to every pixel's RGBA values. It can do hue rotation, saturation changes, colour channel swapping, and more - all in one primitive.

The matrix

The matrix is applied to each pixel's [R, G, B, A, 1] vector. The result is a new [R', G', B', A']:

| R' |   | a00 a01 a02 a03 a04 |   | R |
| G' | = | a10 a11 a12 a13 a14 | × | G |
| B' |   | a20 a21 a22 a23 a24 |   | B |
| A' |   | a30 a31 a32 a33 a34 |   | A |
                                    | 1 |

The 5th column (a04, a14, a24, a34) adds constant offsets - these let you brighten/darken without a multiplication.

Shorthand types

Instead of writing all 20 matrix values, you can use the type attribute for common operations:

<!-- Desaturate (greyscale) -->
<feColorMatrix type="saturate" values="0"/>

<!-- Half saturation -->
<feColorMatrix type="saturate" values="0.5"/>

<!-- Hue rotate by 90 degrees -->
<feColorMatrix type="hueRotate" values="90"/>

<!-- Invert alpha (used for knockout effects) -->
<feColorMatrix type="matrix"
  values="1 0 0 0 0
          0 1 0 0 0
          0 0 1 0 0
          0 0 0 -1 1"/>
Original saturate="0" saturate="0.5" hueRotate="90" hueRotate="180" feColorMatrix type shortcuts Same source shapes, different colour matrix applied to each

feColorMatrix in action: greyscale (saturate=0), desaturation (0.5), and hue rotation (90°, 180°).

The identity matrix

The "do nothing" matrix (useful as a starting point for understanding):

1 0 0 0 0    <!-- R' = 1*R + 0*G + 0*B + 0*A + 0 -->
0 1 0 0 0    <!-- G' = 0*R + 1*G + 0*B + 0*A + 0 -->
0 0 1 0 0    <!-- B' = 0*R + 0*G + 1*B + 0*A + 0 -->
0 0 0 1 0    <!-- A' = 0*R + 0*G + 0*B + 1*A + 0 -->

Modify individual cells to channel-swap, tint, or adjust brightness. For instance, swapping R and B channels:

<feColorMatrix type="matrix"
  values="0 0 1 0 0
          0 1 0 0 0
          1 0 0 0 0
          0 0 0 1 0"/>

Attributes

AttributeValuesDescription
inSourceGraphic | SourceAlpha | result nameInput image
typematrix | saturate | hueRotate | luminanceToAlphaOperation type
valuesdepends on typematrix: 20 numbers (4×5 matrix).
saturate: 0–1+ (0=greyscale, 1=unchanged).
hueRotate: degrees (0–360).
luminanceToAlpha: no values needed.
Ad

7. feComposite & feMerge

These two primitives control how multiple images are combined.

feMerge - simple layering

feMerge stacks inputs on top of each other in order (first feMergeNode is the bottom layer, last is the top). It's the filter equivalent of SVG's natural paint order - later elements draw on top.

<feMerge>
  <feMergeNode in="background-effect"/>   <!-- bottom -->
  <feMergeNode in="middle-effect"/>        <!-- middle -->
  <feMergeNode in="SourceGraphic"/>        <!-- top -->
</feMerge>

feComposite - Porter-Duff operations

feComposite combines exactly two inputs using compositing operators. The operator attribute selects the mode:

OperatorResult keepsUse case
overBoth layers (in draws on top of in2)Default layering (same as feMerge with 2 inputs)
inOnly where both inputs overlapClipping one image to the shape of another
outin only where in2 is absentPunch a hole (knockout effects)
atopin where in2 exists, plus in2 where in is absentTexturing within a shape boundary
xorWhere exactly one input exists (not both)Exclusion / toggle effects
arithmeticCustom blend: k1*i1*i2 + k2*i1 + k3*i2 + k4Fine-grained compositing control
Original feFlood(blue) IN source feFlood(red) OUT source

feComposite operators: "in" clips the flood colour to the source shape; "out" keeps the flood only where the source is absent (inverted).

feComposite attributes

AttributeValuesDescription
inresult name or keywordFirst input (drawn "on top" for over)
in2result name or keywordSecond input
operatorover | in | out | atop | xor | lighter | arithmeticCompositing operation
k1number (default 0)For arithmetic: result = k1×i1×i2 + k2×i1 + k3×i2 + k4
k2number (default 0)Coefficient for input 1
k3number (default 0)Coefficient for input 2
k4number (default 0)Constant added to result

feMerge attributes

Contains one or more <feMergeNode> children:

Attribute (on feMergeNode)ValuesDescription
inresult name or keywordThe input to layer at this position. First node = bottom, last = top.

feOffset attributes

AttributeValuesDescription
inresult name or keywordInput image
dxnumber (default 0)Horizontal offset in filter coordinate units
dynumber (default 0)Vertical offset in filter coordinate units

feFlood attributes

AttributeValuesDescription
flood-colorcolour (default black)The fill colour for the entire filter sub-region
flood-opacity0–1 (default 1)Opacity of the flood

8. feTurbulence - Generating Noise

feTurbulence generates procedural noise (Perlin noise or fractal turbulence) without any input - it creates pixels from nothing. Combined with feDisplacementMap, it can create organic distortion effects (water ripples, paper texture, fire).

<filter id="noise">
  <feTurbulence type="fractalNoise"
    baseFrequency="0.05" numOctaves="3" seed="42"/>
</filter>

<filter id="turbulence">
  <feTurbulence type="turbulence"
    baseFrequency="0.02" numOctaves="4"/>
</filter>

Key attributes at a glance (full table below the demos):

AttributeValuesEffect
typefractalNoiseSmooth, cloud-like noise (good for textures, paper, fog)
turbulenceMore contrasty, wrinkly noise (good for water, marble, fire)
baseFrequencynumber | "x y"Zoom level of the noise. Smaller = larger, blobby features; larger = finer grain. Two values set x and y independently - e.g. "0.05 0.01" produces tall, vertically-stretched features (high frequency horizontally, low vertically). Useful for wood grain, rain, or directional textures.
numOctavesinteger (default 1)Noise layers summed together. More = finer detail but slower.
seednumber (default 0)Random seed. Same seed = reproducible noise pattern.
fractalNoise (0.04, 3 octaves) turbulence (0.03, 4 octaves)

Left: smooth fractal noise. Right: turbulence (more contrast and sharper features).

Using noise for distortion

Combine feTurbulence with feDisplacementMap to distort an element organically:

<filter id="wavy">
  <feTurbulence type="turbulence" baseFrequency="0.02"
    numOctaves="2" result="noise"/>
  <feDisplacementMap in="SourceGraphic" in2="noise"
    scale="15" xChannelSelector="R" yChannelSelector="G"/>
</filter>
Hello SVG No filter Hello SVG feDisplacementMap

Turbulence noise used as a displacement map creates an organic warping effect on text.

feDisplacementMap explained

For each pixel in the output, the displacement map reads the colour value at that position from the in2 image, then shifts the corresponding pixel from in by an amount proportional to that colour value. xChannelSelector picks which colour channel (R, G, B, or A) controls horizontal displacement; yChannelSelector picks the channel for vertical. The scale attribute controls the maximum displacement distance. A neutral value (0.5 in the 0–1 range, or 128 in 0–255) means no displacement.

feTurbulence attributes

AttributeValuesDescription
typefractalNoise | turbulenceNoise algorithm. fractalNoise = smooth/cloudy. turbulence = higher contrast/wrinkly.
baseFrequencynumber | "x y" (required)Frequency of the noise. Smaller = larger features. Two values set x/y independently - e.g. "0.05 0.01" stretches noise vertically (high freq horizontal, low vertical).
numOctavesinteger (default 1)Number of noise layers summed together. More = finer detail but more expensive.
seednumber (default 0)Random seed for reproducible patterns
stitchTilesstitch | noStitchWhether to adjust noise to tile seamlessly at boundaries (default: noStitch)

feDisplacementMap attributes

AttributeValuesDescription
inresult name or keywordThe image to be displaced
in2result name or keywordThe displacement map image (its colour channels drive the displacement)
scalenumber (default 0)Maximum displacement distance. Positive = displace, 0 = no effect.
xChannelSelectorR | G | B | AWhich channel of in2 controls horizontal displacement (default: A)
yChannelSelectorR | G | B | AWhich channel of in2 controls vertical displacement (default: A)

9. Lighting Effects

SVG filters include a lighting model with two surface types and three light source types. Combined, they can create embossed, raised, or 3D effects on flat 2D shapes.

Surface types

Light sources (nested inside lighting elements)

How it works

Lighting primitives use the alpha channel of their input as a height map (bump map). Brighter alpha = higher surface. The lighting calculation then determines how light would illuminate this bumpy surface, producing a lit image. You typically:

  1. Feed the element's alpha (or a noise texture) as input
  2. Apply the lighting primitive to generate a lit surface
  3. Composite the lighting result with the original using feComposite operator="arithmetic"
<filter id="emboss">
  <!-- Use source alpha as the bump map -->
  <feDiffuseLighting in="SourceGraphic" surfaceScale="3"
    diffuseConstant="1" lighting-color="white" result="light">
    <feDistantLight azimuth="225" elevation="45"/>
  </feDiffuseLighting>

  <!-- Combine lighting with original -->
  <feComposite in="SourceGraphic" in2="light"
    operator="arithmetic" k1="1" k2="0" k3="0" k4="0"/>
</filter>
feDiffuseLighting (feDistantLight) feSpecularLighting (fePointLight)

Left: diffuse lighting (matte, even shading). Right: specular lighting (shiny highlight from a point light source).

Lighting is powerful but complex

Getting lighting to look "right" requires tweaking surfaceScale, the constant/exponent values, and the light source position. Start with small surfaceScale values (2–5) and adjust. The arithmetic composite step is where you control how much the lighting affects the final result (k1 controls multiplication of input*light, k2 controls input-only contribution, k3 controls light-only).

feDiffuseLighting attributes

AttributeValuesDescription
inresult name or keywordInput whose alpha channel is used as the bump/height map
surfaceScalenumber (default 1)Height multiplier for the bump map (higher = more pronounced 3D effect)
diffuseConstantnumber (default 1)kd in the Lambertian lighting model. Controls light intensity.
lighting-colorcolour (default white)Colour of the light source

feSpecularLighting attributes

AttributeValuesDescription
inresult name or keywordInput whose alpha channel is used as the bump/height map
surfaceScalenumber (default 1)Height multiplier for the bump map
specularConstantnumber (default 1)ks in the Phong model. Controls highlight intensity.
specularExponentnumber 1–128 (default 1)Shininess exponent. Higher = tighter, more focused highlights.
lighting-colorcolour (default white)Colour of the light source

Light source elements

ElementAttributesDescription
<feDistantLight>azimuth (degrees, default 0), elevation (degrees, default 0)Parallel light from a direction. Azimuth = compass direction (0=right, 90=down). Elevation = angle above the XY plane.
<fePointLight>x, y, z (all default 0)Omnidirectional light from a 3D position. z controls distance "above" the surface.
<feSpotLight>x, y, z, pointsAtX, pointsAtY, pointsAtZ, specularExponent (default 1), limitingConeAngle (default none)Cone of light from (x,y,z) aimed at (pointsAtX/Y/Z). specularExponent controls falloff from centre of cone. limitingConeAngle clips the cone edge.

10. feMorphology - Erode and Dilate

feMorphology shrinks (erode) or expands (dilate) the shape boundaries. It's like a variable-width stroke that can also be used to create outlines, bold text effects, or to thicken/thin elements.

<!-- Thicken (dilate) by 2 units -->
<filter id="thick">
  <feMorphology operator="dilate" radius="2"/>
</filter>

<!-- Thin (erode) by 1 unit -->
<filter id="thin">
  <feMorphology operator="erode" radius="1"/>
</filter>
SVG Text Original SVG Text erode (radius=1) SVG Text dilate (radius=2)

feMorphology: erode shrinks the text, dilate expands it. Useful for creating outlines or adjusting visual weight.

Outline text trick

Combine dilate + original to create a text outline effect (dilate creates the "stroke", then layer the original on top as the fill):

<filter id="text-outline">
  <feMorphology in="SourceGraphic" operator="dilate"
    radius="2" result="expanded"/>
  <!-- Colour the expanded area -->
  <feFlood flood-color="#e06c75" result="outlineColor"/>
  <feComposite in="outlineColor" in2="expanded"
    operator="in" result="outline"/>
  <!-- Layer: outline behind, original on top -->
  <feMerge>
    <feMergeNode in="outline"/>
    <feMergeNode in="SourceGraphic"/>
  </feMerge>
</filter>
OUTLINED

Text outline created with feMorphology dilate + feFlood + feComposite. The dilated shape (red) shows behind the original text (gold).

Attributes

AttributeValuesDescription
inresult name or keywordInput image
operatorerode | dilateShrink or expand shape boundaries
radiusnumber | "x y" (default 0)Radius of the morphology operation. Two values for independent x/y.

11. A Real-World Example: Glow Effect

Combining blur, colour, and compositing - here's a neon glow effect. The approach: blur the source graphic (creates a coloured halo), then layer the crisp original on top:

<filter id="glow">
  <!-- Blur the source to create the glow halo -->
  <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur"/>

  <!-- Brighten the blur (optional: boost glow intensity) -->
  <feColorMatrix in="blur" type="matrix"
    values="1 0 0 0 0
            0 1 0 0 0
            0 0 1 0 0
            0 0 0 2 0"
    result="brightBlur"/>

  <!-- Layer: glow behind, sharp original on top -->
  <feMerge>
    <feMergeNode in="brightBlur"/>
    <feMergeNode in="SourceGraphic"/>
  </feMerge>
</filter>
NEON

Neon glow: blur the source graphic, boost alpha to intensify the halo, layer the crisp original on top. Works with any colour.

Why enlarge the filter region for glow?

Notice x="-20%" y="-20%" width="140%" height="140%" on the filter. The glow extends beyond the element's bounding box (the blurred halo radiates outward). Without the enlarged region, it would get clipped at the edges. Always increase the filter region when your effect expands the visual footprint.

12. Performance & Practical Advice

Reusing filters across elements

A single <filter> defined in <defs> can be referenced by many elements. The filter runs independently for each element (using that element's pixels as input), but you only define the pipeline once. This is the standard pattern - like gradients and clips.

13. Combining Filters with Other SVG Features

Filters play well with everything covered in preceding lessons:

Focus & Blur SMIL animating stdDeviation (0 → 8 → 0, loops)

Animated filter: SMIL animates the blur's stdDeviation from 0 to 8 and back, creating a focus/defocus pulse.

14. Remaining Primitives - Attribute Reference

The following primitives don't have dedicated sections above but are useful to know. Every filter primitive also supports the common attributes in, result, x, y, width, height (to define its sub-region within the filter).

feComponentTransfer

Per-channel transfer functions. Contains child elements <feFuncR>, <feFuncG>, <feFuncB>, <feFuncA>:

Attribute (on feFuncX)ValuesDescription
typeidentity | table | discrete | linear | gammaTransfer function type
tableValueslist of numbersFor table and discrete: defines the lookup values
slopenumber (default 1)For linear: output = slope × input + intercept
interceptnumber (default 0)For linear: constant added after multiplication
amplitudenumber (default 1)For gamma: amplitude × input^exponent + offset
exponentnumber (default 1)For gamma: the power exponent
offsetnumber (default 0)For gamma: constant added after gamma

feBlend

AttributeValuesDescription
inresult name or keywordFirst input
in2result name or keywordSecond input
modenormal | multiply | screen | darken | lighten | overlay | color-dodge | color-burn | hard-light | soft-light | difference | exclusion | hue | saturation | color | luminosityBlend mode (SVG 2 adds the extended CSS blend modes)

feTile

AttributeValuesDescription
inresult name or keywordThe input image to tile. Its sub-region is repeated to fill the filter primitive's sub-region.

feConvolveMatrix

AttributeValuesDescription
inresult name or keywordInput image
orderinteger | "x y" (default 3)Size of the kernel matrix (e.g. "3" = 3×3, "5 3" = 5 columns × 3 rows)
kernelMatrixlist of numbersThe convolution kernel values (order×order numbers, row by row)
divisornumber (default: sum of kernel values, or 1 if sum is 0)Each result pixel is divided by this value (normalisation)
biasnumber (default 0)Added to each result pixel after division
targetXinteger (default: floor(orderX/2))X position of the kernel "hot spot" (0-indexed from left)
targetYinteger (default: floor(orderY/2))Y position of the kernel "hot spot" (0-indexed from top)
edgeModeduplicate | wrap | noneHandling of edge pixels (default: duplicate)
preserveAlphatrue | falseIf true, alpha channel is not convolved (default: false)

feImage

AttributeValuesDescription
href (or xlink:href)URL or fragment referenceExternal image URL or #elementId to use as filter input
preserveAspectRatiosame as <image> (default xMidYMid meet)How the image is fitted into the filter primitive sub-region
crossoriginanonymous | use-credentialsCORS setting for external images

Quiz: Check Your Understanding

Question 1

What does a filter operate on?

The SVG source code (DOM) of the element
The rendered pixels after rasterisation
The geometric path data of the element
The computed CSS styles of the element

Question 2

What is SourceAlpha?

The element's fill colour as a flat buffer
The background behind the filtered element
The alpha channel only: a black silhouette
The element with 50% transparency applied

Question 3

Why does a glow filter need an enlarged filter region?

Glow filters only work on large elements
The browser needs extra space to allocate
The blur halo extends beyond the bounding box
It prevents anti-aliasing artefacts at edges

Question 4

What does feComposite operator="in" do?

Layers the first input on top of the second
Keeps the first input only where the second exists
Blends both inputs using the multiply formula
Inverts the alpha of the first input using second

Hands-On Exercise

Build these from scratch to cement the concepts:

  1. Drop shadow: Create a manual drop shadow pipeline (feGaussianBlur on SourceAlpha → feOffset → feMerge). Apply it to a shape and text element.
  2. Colour manipulation: Use feColorMatrix type="hueRotate" to shift an element's colours by 120 degrees. Then try type="saturate" values="0" for greyscale.
  3. Glow effect: Build a neon glow filter (blur SourceGraphic, boost alpha with feColorMatrix, merge with original). Don't forget to enlarge the filter region.
  4. Distortion: Combine feTurbulence with feDisplacementMap to warp a rectangle or text with organic noise.
<svg width="600" height="500" viewBox="0 0 600 500"
     xmlns="http://www.w3.org/2000/svg">
  <defs>
    <!-- 1. Drop shadow -->
    <filter id="ex-shadow">
      <feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/>
      <feOffset in="blur" dx="4" dy="4" result="shadow"/>
      <feMerge>
        <feMergeNode in="shadow"/>
        <feMergeNode in="SourceGraphic"/>
      </feMerge>
    </filter>

    <!-- 2. Hue rotation -->
    <filter id="ex-hue">
      <feColorMatrix type="hueRotate" values="120"/>
    </filter>

    <!-- 3. Glow -->
    <filter id="ex-glow" x="-20%" y="-20%" width="140%" height="140%">
      <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur"/>
      <feColorMatrix in="blur" type="matrix"
        values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 2 0"
        result="brightBlur"/>
      <feMerge>
        <feMergeNode in="brightBlur"/>
        <feMergeNode in="SourceGraphic"/>
      </feMerge>
    </filter>

    <!-- 4. Distortion -->
    <filter id="ex-warp">
      <feTurbulence type="turbulence" baseFrequency="0.03"
        numOctaves="2" result="noise"/>
      <feDisplacementMap in="SourceGraphic" in2="noise"
        scale="15" xChannelSelector="R" yChannelSelector="G"/>
    </filter>
  </defs>

  <!-- 1. Shadow -->
  <rect x="20" y="20" width="120" height="80" rx="8"
        fill="coral" filter="url(#ex-shadow)"/>

  <!-- 2. Hue rotate -->
  <rect x="200" y="20" width="120" height="80" rx="8"
        fill="coral" filter="url(#ex-hue)"/>

  <!-- 3. Glow -->
  <text x="300" y="220" font-size="36" fill="#61afef"
        filter="url(#ex-glow)">GLOW</text>

  <!-- 4. Distortion -->
  <text x="50" y="400" font-size="36" fill="#e5c07b"
        filter="url(#ex-warp)">Warped Text</text>
</svg>