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.
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>
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.
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
| Attribute | Values | Description |
|---|---|---|
id | string | Unique identifier for referencing via filter="url(#id)" |
x | length (default -10%) | Left edge of filter region |
y | length (default -10%) | Top edge of filter region |
width | length (default 120%) | Width of filter region |
height | length (default 120%) | Height of filter region |
filterUnits | objectBoundingBox | userSpaceOnUse | Coordinate system for x/y/width/height (default: objectBoundingBox) |
primitiveUnits | userSpaceOnUse | objectBoundingBox | Coordinate 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>
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:
SourceGraphic- the full rendered element (colour + alpha) before any filter processingSourceAlpha- only the alpha channel of the source (a silhouette - opaque where the element exists, transparent elsewhere). Perfect for generating shadows.BackgroundImage/BackgroundAlpha- the rendered content behind the filtered element (rarely used; requiresenable-backgroundand has poor browser support)FillPaint/StrokePaint- the element's fill or stroke colour as a flat pixel buffer (useful for lighting effects)
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)
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:
| Category | Primitive | What it does |
|---|---|---|
| Blur & Shadow | feGaussianBlur | Gaussian blur with configurable radius |
feDropShadow | Shorthand: offset + blur + colour (SVG 2) | |
| Colour | feColorMatrix | 4×5 matrix colour transform (hue rotate, saturate, etc.) |
feComponentTransfer | Per-channel transfer functions (gamma, threshold, etc.) | |
feFlood | Fills the filter region with a solid colour | |
| Compositing | feComposite | Porter-Duff compositing (in, out, atop, xor, over, arithmetic) |
feBlend | Blend modes (normal, multiply, screen, darken, lighten) | |
feMerge | Layer multiple inputs on top of each other | |
| Spatial | feOffset | Shifts pixels by dx/dy (used in shadow pipelines) |
feTile | Tiles the input to fill the filter region | |
feDisplacementMap | Distorts pixels using another image as a displacement map | |
| Morphology | feMorphology | Erode or dilate (shrink/expand) shapes |
feConvolveMatrix | Custom convolution kernel (sharpen, emboss, edge-detect) | |
| Lighting | feDiffuseLighting | Matte surface lighting (Lambertian reflection) |
feSpecularLighting | Shiny surface lighting (specular highlights) | |
| Texture | feTurbulence | Generates Perlin noise or turbulence patterns |
| Utility | feImage | Loads 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:
- One value (
stdDeviation="5") - equal blur in both x and y (isotropic, soft glow) - Two values (
stdDeviation="8 0") - first is x-blur, second is y-blur. This lets you blur in one direction only (motion blur) or weight the axes differently.
<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>
Increasing blur radius. The last example uses two values (x-blur, y-blur) to create a directional motion blur.
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
| Attribute | Values | Description |
|---|---|---|
in | SourceGraphic | SourceAlpha | result name | Input image |
stdDeviation | number | "x y" | Blur radius. Single value = equal x/y. Two values = independent horizontal/vertical blur. |
edgeMode | duplicate | wrap | none | How 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:
- feGaussianBlur on
SourceAlpha- creates a blurred silhouette - feOffset - shifts the blurred silhouette down and to the right
- 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>
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 - one element does what the manual pipeline needed five.
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.
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
| Attribute | Values | Description |
|---|---|---|
in | SourceGraphic | SourceAlpha | result name | Input image |
dx | number (default 2) | Horizontal shadow offset |
dy | number (default 2) | Vertical shadow offset |
stdDeviation | number | "x y" (default 2) | Blur radius for the shadow |
flood-color | colour (default black) | Shadow colour |
flood-opacity | 0–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"/>
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
| Attribute | Values | Description |
|---|---|---|
in | SourceGraphic | SourceAlpha | result name | Input image |
type | matrix | saturate | hueRotate | luminanceToAlpha | Operation type |
values | depends on type | matrix: 20 numbers (4×5 matrix). saturate: 0–1+ (0=greyscale, 1=unchanged). hueRotate: degrees (0–360). luminanceToAlpha: no values needed. |
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:
| Operator | Result keeps | Use case |
|---|---|---|
over | Both layers (in draws on top of in2) | Default layering (same as feMerge with 2 inputs) |
in | Only where both inputs overlap | Clipping one image to the shape of another |
out | in only where in2 is absent | Punch a hole (knockout effects) |
atop | in where in2 exists, plus in2 where in is absent | Texturing within a shape boundary |
xor | Where exactly one input exists (not both) | Exclusion / toggle effects |
arithmetic | Custom blend: k1*i1*i2 + k2*i1 + k3*i2 + k4 | Fine-grained compositing control |
feComposite operators: "in" clips the flood colour to the source shape; "out" keeps the flood only where the source is absent (inverted).
feComposite attributes
| Attribute | Values | Description |
|---|---|---|
in | result name or keyword | First input (drawn "on top" for over) |
in2 | result name or keyword | Second input |
operator | over | in | out | atop | xor | lighter | arithmetic | Compositing operation |
k1 | number (default 0) | For arithmetic: result = k1×i1×i2 + k2×i1 + k3×i2 + k4 |
k2 | number (default 0) | Coefficient for input 1 |
k3 | number (default 0) | Coefficient for input 2 |
k4 | number (default 0) | Constant added to result |
feMerge attributes
Contains one or more <feMergeNode> children:
| Attribute (on feMergeNode) | Values | Description |
|---|---|---|
in | result name or keyword | The input to layer at this position. First node = bottom, last = top. |
feOffset attributes
| Attribute | Values | Description |
|---|---|---|
in | result name or keyword | Input image |
dx | number (default 0) | Horizontal offset in filter coordinate units |
dy | number (default 0) | Vertical offset in filter coordinate units |
feFlood attributes
| Attribute | Values | Description |
|---|---|---|
flood-color | colour (default black) | The fill colour for the entire filter sub-region |
flood-opacity | 0–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):
| Attribute | Values | Effect |
|---|---|---|
type | fractalNoise | Smooth, cloud-like noise (good for textures, paper, fog) |
turbulence | More contrasty, wrinkly noise (good for water, marble, fire) | |
baseFrequency | number | "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. |
numOctaves | integer (default 1) | Noise layers summed together. More = finer detail but slower. |
seed | number (default 0) | Random seed. Same seed = reproducible noise pattern. |
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>
Turbulence noise used as a displacement map creates an organic warping effect on text.
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
| Attribute | Values | Description |
|---|---|---|
type | fractalNoise | turbulence | Noise algorithm. fractalNoise = smooth/cloudy. turbulence = higher contrast/wrinkly. |
baseFrequency | number | "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). |
numOctaves | integer (default 1) | Number of noise layers summed together. More = finer detail but more expensive. |
seed | number (default 0) | Random seed for reproducible patterns |
stitchTiles | stitch | noStitch | Whether to adjust noise to tile seamlessly at boundaries (default: noStitch) |
feDisplacementMap attributes
| Attribute | Values | Description |
|---|---|---|
in | result name or keyword | The image to be displaced |
in2 | result name or keyword | The displacement map image (its colour channels drive the displacement) |
scale | number (default 0) | Maximum displacement distance. Positive = displace, 0 = no effect. |
xChannelSelector | R | G | B | A | Which channel of in2 controls horizontal displacement (default: A) |
yChannelSelector | R | G | B | A | Which 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
feDiffuseLighting- matte surface (Lambertian reflection). Light scatters evenly in all directions. The result is a smooth, clay-like appearance.feSpecularLighting- shiny surface (Phong-like specular highlights). Light reflects directionally, creating bright hotspots. Think plastic or metal.
Light sources (nested inside lighting elements)
feDistantLight- parallel rays from a direction (like the sun). Defined byazimuthandelevationangles.fePointLight- omnidirectional from a position (like a bare bulb). Defined byx,y,z.feSpotLight- cone of light from a position aimed at a target. HaspointsAtX/Y/Z,limitingConeAngle, andspecularExponent.
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:
- Feed the element's alpha (or a noise texture) as input
- Apply the lighting primitive to generate a lit surface
- 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>
Left: diffuse lighting (matte, even shading). Right: specular lighting (shiny highlight from a point light source).
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
| Attribute | Values | Description |
|---|---|---|
in | result name or keyword | Input whose alpha channel is used as the bump/height map |
surfaceScale | number (default 1) | Height multiplier for the bump map (higher = more pronounced 3D effect) |
diffuseConstant | number (default 1) | kd in the Lambertian lighting model. Controls light intensity. |
lighting-color | colour (default white) | Colour of the light source |
feSpecularLighting attributes
| Attribute | Values | Description |
|---|---|---|
in | result name or keyword | Input whose alpha channel is used as the bump/height map |
surfaceScale | number (default 1) | Height multiplier for the bump map |
specularConstant | number (default 1) | ks in the Phong model. Controls highlight intensity. |
specularExponent | number 1–128 (default 1) | Shininess exponent. Higher = tighter, more focused highlights. |
lighting-color | colour (default white) | Colour of the light source |
Light source elements
| Element | Attributes | Description |
|---|---|---|
<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>
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>
Text outline created with feMorphology dilate + feFlood + feComposite. The dilated shape (red) shows behind the original text (gold).
Attributes
| Attribute | Values | Description |
|---|---|---|
in | result name or keyword | Input image |
operator | erode | dilate | Shrink or expand shape boundaries |
radius | number | "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 glow: blur the source graphic, boost alpha to intensify the halo, layer the crisp original on top. Works with any colour.
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
- Filters are expensive. Every filtered element gets its own offscreen raster buffer. The browser must rasterise the element, run all primitives, then composite the result. Avoid filters on elements that animate at 60fps unless GPU-composited (or use CSS
filterfor simple cases). - CSS
filtervs SVGfilter: CSS provides shortcuts for common filters:filter: blur(5px) drop-shadow(3px 3px 5px black) saturate(1.5). These are typically GPU-accelerated - the browser runs them as GPU shaders on a compositor layer (same pipeline astransformandopacity), so they don't block the main thread. SVG filters run during the CPU paint step and process primitives sequentially. The visual result is identical, but CSS filters animate more cheaply. Trade-off: CSS filter only offers a fixed set of predefined effects, while SVG filters give you full pipeline control (17 primitives, arbitrary chaining). Use CSS filter for simple effects on animated elements; use SVG filter when you need the full power or are working inside a self-contained SVG. - filterUnits:
userSpaceOnUse(filter region in element's coordinate system) vsobjectBoundingBox(default; region as fraction of bounding box). For reusable filters that work on different-sized elements, the defaultobjectBoundingBoxis usually what you want. - primitiveUnits: Controls the coordinate system for individual primitive attributes (like stdDeviation, radius, scale). Default is
userSpaceOnUse, which means blur radius etc. are in the element's coordinate units. - Debugging tip: To see what a specific intermediate result looks like, temporarily make it the final output by removing subsequent primitives. Or use an
feMergeat the end that only references the intermediateresultname you want to inspect.
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:
- Filters + Clips: Apply
clip-pathbefore filtering (the element is clipped, then the clipped result is filtered). Or filter first, then clip the result using CSSclip-pathon the element. - Filters + Masks: Similar to clips. The mask is applied, producing a partially-transparent element, which is then fed into the filter pipeline.
- Filters + Groups: Apply
filterto a<g>element to filter the entire group as one composited image. This is efficient and lets you create effects that span multiple shapes. - Filters + Animation (SMIL): You can animate filter primitive attributes with SMIL! Animate
stdDeviationfor a focus/unfocus effect, orbaseFrequencyinfeTurbulencefor animated noise. (We'll explore this in Lessons 9–10.)
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) | Values | Description |
|---|---|---|
type | identity | table | discrete | linear | gamma | Transfer function type |
tableValues | list of numbers | For table and discrete: defines the lookup values |
slope | number (default 1) | For linear: output = slope × input + intercept |
intercept | number (default 0) | For linear: constant added after multiplication |
amplitude | number (default 1) | For gamma: amplitude × input^exponent + offset |
exponent | number (default 1) | For gamma: the power exponent |
offset | number (default 0) | For gamma: constant added after gamma |
feBlend
| Attribute | Values | Description |
|---|---|---|
in | result name or keyword | First input |
in2 | result name or keyword | Second input |
mode | normal | multiply | screen | darken | lighten | overlay | color-dodge | color-burn | hard-light | soft-light | difference | exclusion | hue | saturation | color | luminosity | Blend mode (SVG 2 adds the extended CSS blend modes) |
feTile
| Attribute | Values | Description |
|---|---|---|
in | result name or keyword | The input image to tile. Its sub-region is repeated to fill the filter primitive's sub-region. |
feConvolveMatrix
| Attribute | Values | Description |
|---|---|---|
in | result name or keyword | Input image |
order | integer | "x y" (default 3) | Size of the kernel matrix (e.g. "3" = 3×3, "5 3" = 5 columns × 3 rows) |
kernelMatrix | list of numbers | The convolution kernel values (order×order numbers, row by row) |
divisor | number (default: sum of kernel values, or 1 if sum is 0) | Each result pixel is divided by this value (normalisation) |
bias | number (default 0) | Added to each result pixel after division |
targetX | integer (default: floor(orderX/2)) | X position of the kernel "hot spot" (0-indexed from left) |
targetY | integer (default: floor(orderY/2)) | Y position of the kernel "hot spot" (0-indexed from top) |
edgeMode | duplicate | wrap | none | Handling of edge pixels (default: duplicate) |
preserveAlpha | true | false | If true, alpha channel is not convolved (default: false) |
feImage
| Attribute | Values | Description |
|---|---|---|
href (or xlink:href) | URL or fragment reference | External image URL or #elementId to use as filter input |
preserveAspectRatio | same as <image> (default xMidYMid meet) | How the image is fitted into the filter primitive sub-region |
crossorigin | anonymous | use-credentials | CORS setting for external images |
Quiz: Check Your Understanding
Question 1
What does a filter operate on?
Question 2
What is SourceAlpha?
Question 3
Why does a glow filter need an enlarged filter region?
Question 4
What does feComposite operator="in" do?
Hands-On Exercise
Build these from scratch to cement the concepts:
- Drop shadow: Create a manual drop shadow pipeline (feGaussianBlur on SourceAlpha → feOffset → feMerge). Apply it to a shape and text element.
- Colour manipulation: Use
feColorMatrix type="hueRotate"to shift an element's colours by 120 degrees. Then trytype="saturate" values="0"for greyscale. - Glow effect: Build a neon glow filter (blur SourceGraphic, boost alpha with feColorMatrix, merge with original). Don't forget to enlarge the filter region.
- Distortion: Combine
feTurbulencewithfeDisplacementMapto 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>