How to understanding svg <defs>, <symbol>, and <clippath> elements
- Step 1Locate the <defs> block — Open the SVG in a text editor.
<defs>usually sits near the top, after the<svg>opening tag. Its common children:linearGradient,radialGradient,clipPath,filter,mask,marker,pattern,symbol. Each meaningful one carries anid. - Step 2Identify the reference grammar for each child — Paint, clip, mask, filter, and marker definitions are referenced with
url(#id)inside attributes likefill,stroke,clip-path,mask,filter,marker-end. Symbols and gradient chaining usehref="#id"orxlink:href="#id". These are the only two forms the purger looks for. - Step 3Trace one id across the whole document — Pick
id="grad-1"and search the entire file forurl(#grad-1)andhref="#grad-1". Search everywhere — including inside other<defs>children — because a filter or gradient can reference another def. If you find at least one match, it is live. - Step 4Note the def-to-def cascade trap — If
grad-1is referenced only insideclipPath-x, andclipPath-xis itself referenced by nothing, then both are effectively dead — but a single text scan sees theurl(#grad-1)insideclipPath-xand counts it as live. This is why the purger needs a second pass to collect such transitive orphans. - Step 5Check for id-less and external references — A
<defs>child with noidcan never be matched, so it is always kept. An id referenced only from external CSS or JavaScript looks dead to any document-only scan. Both are real cases where automatic detection diverges from true usage. - Step 6Run the purger to apply the same logic automatically — Once you understand the reference test, the Unused Defs Purger performs it for you in one click and reports
Defs removed. Verify against your hand-trace for confidence.
The <defs> children and how they are referenced
Each definition type, its purpose, and the exact reference grammar that keeps it alive. The purger matches the two grammars in the right-hand column.
| Element | What it does | Referenced via | Renders directly? |
|---|---|---|---|
<linearGradient> / <radialGradient> | Defines a colour ramp used as paint | fill="url(#id)", stroke="url(#id)", or xlink:href="#id" (chaining) | No — only when painted |
<clipPath> | Defines a region; pixels outside are clipped | clip-path="url(#id)" | No |
<mask> | Defines a luminance/alpha mask | mask="url(#id)" | No |
<filter> | Defines a pixel effect (blur, shadow, etc.) | filter="url(#id)" | No |
<marker> | Defines an arrowhead/dot for path ends | marker-start/-mid/-end="url(#id)" | No |
<pattern> | Defines a tiled fill | fill="url(#id)" | No |
<symbol> | Defines a reusable template with its own viewBox | <use href="#id"> / xlink:href="#id" | No — only via <use> |
<symbol> vs <g> vs an id-less def child
Three things that look similar in markup but behave very differently for both rendering and dead-code analysis.
| Construct | Has own viewport/viewBox? | Rendered directly? | Purger treatment |
|---|---|---|---|
<symbol id="x"> | Yes | No — only through <use> | Removed if no <use href="#x"> / fill references it |
<g> (outside defs) | No | Yes — drawn in parent coordinates | Not a defs child; never touched |
<defs> child with no id | n/a | No | Always kept — cannot be matched against references |
Cookbook
Reference-tracing walkthroughs — the same matching the purger does, shown by hand so you can predict the outcome.
A live gradient: traced by url(#id)
Search the document for url(#brand). One match on the rect — so brand is live and the purger keeps it.
<defs> <linearGradient id="brand">…</linearGradient> </defs> <rect fill="url(#brand)" width="100" height="40"/> Hand-trace: grep url(#brand) → 1 match (rect fill). Verdict: LIVE. Purger keeps it.
A dead clipPath: no url() reference exists
Nothing in the file carries clip-path="url(#crop)". Zero matches → dead → removed.
<defs> <clipPath id="crop"><rect width="50" height="50"/></clipPath> </defs> <circle r="30" fill="#0a0"/> Hand-trace: grep url(#crop) → 0 matches. Verdict: DEAD. Purger removes it (Defs removed: 1).
Gradient chaining keeps a base alive via href
skin reuses base through xlink:href. Tracing base finds the href match, so base is live even with no shape using it directly.
<defs> <linearGradient id="base"><stop stop-color="#f06"/></linearGradient> <linearGradient id="skin" xlink:href="#base" gradientTransform="rotate(30)"/> </defs> <path fill="url(#skin)" d="…"/> Hand-trace base: grep "#base" → 1 href match. Verdict: base LIVE (via href), skin LIVE (via url). Both kept.
The cascade trap a single pass cannot see
halo (a filter) is referenced by nothing. Inside halo's body is feImage href="#tex". Tracing tex finds that href and marks it live — even though halo, its only consumer, is about to be deleted.
<defs> <filter id="halo"><feImage href="#tex"/></filter> <pattern id="tex">…</pattern> </defs> <!-- nothing references #halo --> Pass 1: halo DEAD (removed). tex still LIVE (href seen inside halo). Pass 2: tex now DEAD (its only referrer is gone).
An id-less filter is invisible to the analysis
A filter with no id cannot appear in any url(#id), so it can never be matched — the purger always keeps it, even though nothing could ever reference it.
<defs> <filter><feGaussianBlur stdDeviation="2"/></filter> </defs> Hand-trace: no id to search for. Verdict: KEPT unconditionally (not a removal candidate).
Edge cases and what actually happens
A def is referenced only inside another def
By designThe reference scan reads the whole document, so url(#x) written inside another <defs> child counts as a live reference to x. This correctly preserves filter-internal gradients — but it also means a def kept alive solely by a doomed referrer survives the first pass. Run the purger twice to resolve the chain.
Id-less definition that is genuinely dead
PreservedOnly id-bearing <defs> children are evaluated. A definition with no id cannot be addressed and is never removed, even if it is dead weight. To let the purger drop it, give it an id first (then it becomes a normal candidate).
Reference written without url() wrapper
Reference failSVG paint references must use the url(#id) form; the purger's scan requires it. A malformed fill="#grad" (no url()) is not recognised as a reference, so the gradient is judged dead. Well-formed SVG always uses url(), so this only bites hand-edited markup.
Symbol referenced from a different file
Removed in errorA <symbol> used only by <use href="#icon"> on another page has no in-file reference, so the purger marks it dead. Document-only analysis cannot see cross-file usage. Keep shared sprites intact and assemble them with the SVG Sprite Builder.
Id referenced from external CSS
Reference failA stylesheet doing fill: url(#grad) outside the SVG is not part of the document the purger reads, so grad looks dead and is removed. Inline the style, or do not purge SVGs whose defs are styled externally.
Multiple <defs> blocks in one file
SupportedSome exporters emit more than one <defs>. The scan collects defs > * across all of them and evaluates each id-bearing child the same way. Any <defs> left empty after purging is removed.
Shape placed inside <defs> instead of being referenced
Watch closelyA plain shape (e.g. <path id="icon">) parked in <defs> renders nothing unless a <use href="#icon"> pulls it out. If no <use> references it, the purger removes it as unused — which is usually correct, but surprising if you intended to reference it later.
Same id used twice (duplicate ids)
Watch closelyDuplicate ids are invalid SVG and the browser resolves references to the first match. The purger keeps any def whose id appears in a reference, so both duplicates may be retained. Fix duplicate ids before relying on the artwork — they cause unpredictable rendering independent of any purge.
Frequently asked questions
What exactly is <defs> for?
It is a non-rendering container for reusable definitions — gradients, clip-paths, masks, filters, markers, patterns, and symbols. Nothing inside <defs> paints on its own; it only appears when another element references its id via url(#id) or href="#id".
What is the difference between <defs> and inline elements?
An element inside <defs> is a template that renders only when referenced and can be reused many times. An inline element (a <rect> directly in the canvas) is drawn immediately where it appears and is not reusable by reference.
Can I put any element inside <defs>?
Technically yes, but only reuse-oriented elements (gradient, pattern, filter, clipPath, mask, marker, symbol) belong there. A plain shape inside <defs> is invisible unless a <use> references it — and the purger will remove it if nothing does.
What is the difference between <symbol> and <g>?
<symbol> has its own viewport and viewBox and never renders directly — only through <use href="#id">. <g> is a grouping element rendered in the parent coordinate space. Use <symbol> for sprite components and <g> for grouping live artwork.
How are gradients referenced?
As paint via fill="url(#id)" or stroke="url(#id)", and chained to another gradient via xlink:href="#id" (to reuse its stops). The purger recognises both forms, so a chained base gradient is kept even with no direct shape reference.
Why do Figma exports have so many unused gradient defs?
Figma pre-generates gradients for component states and variants. Exporting a single frame can carry along gradients for variants you didn't export, leaving several linearGradient defs that nothing in the file references — exactly what the purger removes.
How do I tell by hand whether a def is used?
Pick its id and search the whole file for url(#id) and href="#id" (including inside other defs). One match means live; zero means dead. That is precisely the test the purger automates.
If a filter uses a gradient internally, will the gradient be kept?
Yes. The reference scan reads the full document, so a url(#grad) written inside a <filter> body counts as a reference to grad and keeps it alive. This is the indirect-reference case working correctly.
What happens to a def that has no id?
It is always kept. The tool only considers id-bearing <defs> children for removal, because a def with no id cannot be matched against any reference.
Does removing unused defs ever change the rendered image?
It should not — only definitions nothing references are removed. If the image changes, a reference existed somewhere the document-only scan can't see: external CSS, JavaScript, or another file.
Can the analysis follow a reference into a second def and then a third?
In one pass it sees all references as flat text, so a multi-level chain (A uses B uses C) keeps B and C alive as long as their ids appear anywhere. But if the top of the chain is itself dead, lower links only become removable on a subsequent pass. Run the purger twice for deep chains.
Which tool actually performs this cleanup?
The Unused Defs Purger. This page explains the model; the tool applies it in one click and reports how many defs it removed.
Privacy first
Every JAD SVG tool runs entirely in your browser using the DOM API and Canvas. Your SVG files never leave your device — verified by zero outbound network requests during processing.