Designing Safe SVG Inlining with SVGO's prefixIds: An Architecture Note
Inlining multiple SVGs into one document makes element IDs collide globally. An architecture note on using SVGO's prefixIds plugin at build time: the requirement, minimal deterministic config, trust boundaries, and failure modes.
30 Dec 2025, 20:43 UTC

If you inline more than one SVG into the same HTML document, you inherit a subtle bug factory: SVG element IDs live in the document's global namespace, not inside each SVG. Two icons that both contain <linearGradient id="a"> will collide, and the second icon will quietly render with the first icon's gradient. SVGO's prefixIds plugin exists to solve exactly this problem at build time. This note covers the requirement, the smallest design that satisfies it, where the trust boundaries sit, and the failure modes that should make you reconsider.
The requirement: many SVGs, one document, no collisions
The typical scenario is a build pipeline that optimizes a folder of SVG assets and inlines them into a page — an icon system, a sprite sheet, a CMS that embeds author-uploaded diagrams. The requirements are:
- Every SVG must keep working when several are inlined into one DOM.
- Internal references (
url(#gradient),href="#shape",aria-labelledby) must keep pointing at the right element after IDs are rewritten. - The transformation must be automatic — asset authors should not have to hand-namespace IDs.
- The pipeline must be repeatable: the same input produces the same output, so diffs and caches stay stable.
The smallest suitable design
Run SVGO as a build-time step with prefixIds enabled and a deterministic prefix derived from the file. SVGO parses the SVG, finds every id attribute, prepends the prefix, and — critically — rewrites the internal references that point at those IDs. That reference rewriting is the reason to use the plugin rather than a regex over the file: a regex that renames id="a" but misses fill="url(#a)" produces a broken image that still looks plausible in a quick review.
A minimal configuration, saved as svgo.config.js in the project root and run with the SVGO CLI (version 3.x assumed; check svgo --version):
module.exports = {
plugins: [
{
name: 'prefixIds',
params: {
prefix: (node, info) => info.path
? info.path.replace(/[^a-zA-Z0-9]/g, '-') + '__'
: 'svg__'
}
}
]
};Run it from the project root with npx svgo -f ./assets/svg -o ./dist/svg. You need write access to the output directory; the tool overwrites nothing outside -o. The prefix function receives file information, so each asset gets a distinct, deterministic namespace derived from its filename — icons/arrow.svg might produce IDs like icons-arrow__gradient-1. Determinism matters: a random prefix would change the output on every build, busting caches and polluting diffs.
Verification after the run is straightforward: open an output file and confirm every id= value carries the prefix and every url(#...) and href="#..." reference carries the same prefix. A quick check from the shell: grep -o 'id="[^"]*"' dist/svg/icon.svg and grep -o 'url(#[^)]*)' dist/svg/icon.svg — the suffixes after the prefix should match one-to-one.
Trust and data boundaries
The key boundary is between internal references, which the plugin owns and rewrites, and external references, which it cannot see. Anything outside the SVG file that addresses an element by ID — a CSS rule like #play-button { fill: red; }, a script calling document.getElementById('play-button'), an HTML anchor linking to #diagram-3 — will silently break when the ID is renamed. The plugin has no way to know these exist.
That defines the rule for where prefixIds belongs:
- Safe: self-contained assets consumed only as whole images or inline blobs — icon sets, illustrations, sprite sources.
- Unsafe without extra care: SVGs that are part of a styled, scripted document where IDs are an API surface. There, either disable the plugin for those files (SVGO supports per-file overrides via separate config or folder-specific runs) or treat the prefixed ID as the public contract and update the CSS/JS accordingly.
A second boundary is input trust. SVGO parses XML; run it on author-supplied files in CI or at upload time, not in the request path of a server, and keep the dependency pinned and updated like any parser handling untrusted input.
Operational checks
Build-time renaming fails silently by design — nothing throws when a reference dangles. So the pipeline needs its own checks:
- Render diff: rasterize a sample page (or each icon) before and after enabling the plugin and compare. A missing gradient or mask shows up immediately as a visual regression; a screenshot comparison in CI (Playwright or similar) catches it automatically.
- Reference audit: a small script that parses each output file, collects all
idvalues, and asserts everyurl(#...)/href="#..."target exists. This catches both plugin edge cases and hand-authored broken references. - Collision audit: concatenate all output SVGs, extract IDs, and assert uniqueness. This is the direct test of the original requirement.
Failure modes
Order-dependent styling or scripting. Some SVGO plugins reorder structure (for example, group-related optimizations can move elements). If your CSS uses sibling selectors or your scripts walk the DOM by position, enable plugins one at a time and re-run the render diff. prefixIds itself does not reorder, but it rarely runs alone.
Non-deterministic prefixes. If the prefix function incorporates a hash of file contents, renaming an ID after a tiny edit changes every ID in the file. Prefer filename-derived prefixes for stable output.
References in attributes the plugin doesn't rewrite. References can hide in places like inline style attributes, begin="anim1.end" SMIL timing, or filter="url(#f)" inside presentation attributes. Current SVGO versions handle the common cases, but the reference audit above is what tells you whether your specific assets hit an uncovered one — don't assume coverage, measure it.
Server-side rendering vs. client hydration mismatches. If SVGs are optimized at build time but a CMS injects unoptimized copies at runtime, prefixed and unprefixed versions of the same asset can coexist and collide anyway. The fix is to enforce the boundary: all inlined SVG goes through the same optimization step.
When this design is the wrong one
Two conditions should push you to a different approach. First, if IDs are a deliberate styling API — theming hooks, animation targets — renaming them at build time fights the design; instead, namespace by convention (authors write id="arrow-icon__gradient") and lint for it, using prefixIds only on the untrusted subset. Second, if assets are inlined once per page and never combined, the collision risk doesn't exist and the plugin only adds diff noise — the smallest suitable design there is to leave IDs alone and keep cleanupIds (which removes genuinely unused IDs) as the only ID-related step. The right test is the collision audit: if it passes without the plugin, you don't need it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.