Avoiding SVG ID Collisions When Inlining Multiple Icons With SVGO
When you inline many SVGs into one HTML page, duplicate element IDs silently break gradients, clip paths, and masks. This post shows how to enable SVGO’s prefixIds plugin to namespace IDs per file, with a concrete config and verification steps.
22 Jan 2026, 04:16 UTC

You’re building an icon system. Instead of sprinkling <img> tags across your pages, you inline SVG markup directly into HTML. It’s a common pattern for control over color, hover states, and caching. But when two inlined SVGs both define id="a" for a gradient or clipPath, the browser doesn’t raise an error. It simply resolves url(#a) to the first element with that ID in the document. The second icon’s gradient goes missing, a clipPath silently fails, and you’re left wondering why your carefully designed icons look broken.
This isn’t a bug in your markup or your build tool. It’s the natural behavior of the DOM when multiple elements share the same ID. SVGO, the popular SVG optimizer, can rewrite those IDs before they ever hit the document, but it doesn’t do it by default.
Thesis: Enable the prefixIds Plugin to Namespace IDs Per File
The fix is straightforward: configure SVGO’s prefixIds plugin (and its companion prefixClassNames) so each inlined SVG gets a unique prefix derived from its filename or a custom pattern. After optimization, every id and every url(#...) or href="#..." reference carries that prefix, so url(#a) from one file becomes url(#icon-a) and never collides with url(#icon-about-a) from another.
One Practical Configuration
Assume you have a directory of icons, each stored as its own SVG file. Your build step runs SVGO before inlining. Here’s a minimal svgo.config.js that adds the plugin to the default preset:
const svgoConfig = {
plugins: [
'preset-default',
{
name: 'prefixIds',
params: {
prefix: 'icon-'
}
},
{
name: 'prefixClassNames',
params: {
prefix: 'icon-'
}
}
]
}
module.exports = svgoConfigRun it like any SVGO invocation, from your project root with SVGO installed (locally via npx svgo or as a devDependency):
npx svgo input.svg --config svgo.config.js -o output.svgIf you’re using a task runner like gulp or webpack, point its SVGO integration at this config. The prefix value can be a static string, or a function that receives the file path and returns a deterministic prefix — useful when you want the prefix to encode the component name or folder depth. Check your installed SVGO major version’s README for the exact parameter shape, since plugin names and params changed between v2 and later majors.
Worked Example: Verifying the Fix
Create two tiny SVGs that each define an ID a with a different gradient:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<defs>
<linearGradient id="a">
<stop offset="0" stop-color="red" />
<stop offset="1" stop-color="blue" />
</linearGradient>
</defs>
<rect width="10" height="10" fill="url(#a)" />
</svg>Name the first icon-home.svg and the second icon-about.svg (with a different gradient in the second file). Inline both into a single HTML page without SVGO optimization and open it in a browser. You’ll likely see the second rectangle lose its gradient because url(#a) resolved to the first definition in the document.
Now run SVGO on each file with the config above. Inspect the output: every id="a" becomes id="icon-a", and every url(#a) becomes url(#icon-a). Inline the optimized SVGs into the same HTML. Both rectangles now render their correct gradients because the IDs are namespaced per file and the DOM no longer has duplicate bare IDs. A quick sanity check: search the final HTML for id="a" — it should return zero matches.
Trade-off and Limitation
Prefixing IDs increases file size slightly — each occurrence of a previously short ID grows by the length of the prefix. In most icon systems that’s negligible, but if you’re optimizing thousands of tiny SVGs against a byte budget, measure the impact.
More importantly, any external CSS or JavaScript that selects elements by the original bare ID (e.g., #a { ... }) will stop matching after optimization. If such code exists in your codebase, update the selectors or keep a mapping. Also, the default preset is a curated bundle, and adding plugins can interact with other optimizations — review the full plugin list before batch-processing production assets.
Finally, if you use a prefix function, it must be deterministic and produce a unique string per file. If two files map to the same prefix, collisions return. Stick to a static prefix or a documented path-derived function, and verify the output once before committing to the config.
Closing: Make It Part of Your Build
Adding prefixIds and prefixClassNames to your SVGO config is a one-time change that pays off every time you inline multiple SVGs. Pair it with a simple verification step — optimize two SVGs with duplicate IDs, inline them, and confirm each renders correctly — and you’ll avoid the silent gradient-and-clipPath failures that often surface late in a project. If sprite-based approaches suit your workflow better, remember that <symbol>/<use> scopes content differently and has its own styling and external-reference limitations; for many component-driven icon systems, prefixIds is the simpler, more direct fix.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.