Why Inline SVG Icons Show the Wrong Gradient (or None at All)
Each SVG looks right when you open it on its own. Put two or three of them inline on the same page and something breaks: an icon shows another icon's gradient, a chart's clip path cuts the wrong shape, a mask hides everything, or an icon goes flat. Remove one SVG and the others come back. The cause is almost always the same. Two files define the same id, and on a single page they are no longer separate.
One page, one ID namespace
Gradients, clip paths, masks, filters, markers and patterns are defined once, given an ID, and referenced from elsewhere: fill="url(#a)", clip-path="url(#clip0)", <use href="#shape">. Inside a standalone .svg file, those IDs only have to be unique within that file. Once you paste the markup into HTML (by hand, through a CMS, or with a framework component that inlines SVGs), every inlined SVG becomes part of one document, and the whole page shares one set of IDs.
Duplicate IDs are invalid, so there is no well-defined winner. Browsers resolve url(#a) to one of the definitions for every element that asks for it. Reports differ by browser: QuickChart users saw the first definition in the document win, and Figma users saw Safari use the last. Either way, at least one icon is drawn with a definition from a different file.
That's also why the problem disappears when the same files are used through <img src="icon.svg">, a CSS background-image or <object>. Each of those loads the SVG as its own document with its own IDs. Inline SVG vs <img> vs CSS background covers when inlining is worth that trade-off.
What it looks like
- Wrong colors. The second icon renders with the first icon's gradient. It's the classic case: both files call their gradient
a, orpaint0_linear. - Shapes cut off or missing. A
clipPathfrom another file, sized for a different drawing, clips most of this one away. Charts are prone to this, because they clip their plot area. - Icon blank or flat. It received a mask or filter meant for something else.
- It breaks only on some pages, or only when the menu is closed. The definition it resolved to lives in an element hidden with
display: none, such as a closed mobile menu, an inactive tab, or a hidden sprite. Gradients and masks defined inside adisplay: nonesubtree commonly fail to render, so every icon that resolves to them loses the effect too.
Where the duplicate IDs come from
Nobody types id="a" in two files on purpose. Tools generate it, and they generate the same names in every file:
- SVGO's
cleanupIdsplugin. It is part of SVGO's default preset, and by default it minifies referenced IDs to short names. Each file is optimized separately, so the first gradient in every file can end up asa. Optimizing makes collisions more likely, not less. Docusaurus had a long-running issue about exactly this: two gradients with distinct names went in, and both came out asa. - Design-tool exports. Figma names clip paths like
clip0and gradientspaint0_linear, plus a suffix, in every export. Illustrator numbers themSVGID_1_,SVGID_2_, starting again in each file. Ten icons from the same design file can easily share aclip0. - Chart and diagram generators. Libraries that emit SVG often use fixed names such as
clip1for the plot area, so two charts on one page collide by construction. - The same file inlined twice. Two copies of one icon duplicate every ID. The definitions are identical, so this is usually harmless, but the
display: nonetrap above still applies.
How to fix it
The fix is to give each file's IDs a unique prefix and update every reference to match. Pick whichever of these fits how your SVGs reach the page:
- At build time, with SVGO: enable the
prefixIdsplugin, which is not in the default preset. It prefixes every ID and reference with a per-file string. Alternatively, keepcleanupIdsbut turn off itsminifyoption, which keeps your meaningful names but doesn't protect against two files that already share one. - In React and similar frameworks: SVGR can apply
prefixIdsthrough its SVGO config, and hand-written components can build IDs with React'suseId()so every rendered instance is unique. - For a folder of files you just need to fix: run them all through the bulk SVG ID collision checker. It lists every ID that more than one file defines, then rewrites each file's IDs with its own prefix, including
url(#…),href,aria-labelledbyand selectors in<style>. You can see exactly which files clash before deciding anything. - If you don't need to style the SVG from the page: use
<img>instead of inlining. There's no shared namespace and nothing to rename.
For the hidden-definition case, the fix is different. A sprite or icon set that other icons reference shouldn't be hidden with display: none. Keep it rendered but invisible, for example as an <svg> with width="0" height="0" and position: absolute, so its gradients and masks stay usable.
How to confirm it's an ID problem
In DevTools, select the broken icon's shape and look at its fill, clip-path or mask. Then search the page's Elements panel for that ID (id="a", say). More than one match means a collision. A quicker test is to rename the ID in one file by hand and reload. If the icon fixes itself, you've found it. For a whole icon set, checking files pairwise doesn't scale, because the collision only exists between files. Drop them all into the checker at once and it builds the list of shared IDs for you. If you optimize your SVGs too, run the optimizer first and prefix afterwards. Prefixing changes only IDs and references, so the order doesn't undo either step.