Guide
Cleaning ugly SVGs before you ship
Remove editor cruft, flatten transforms, fix precision, and make SVGs currentColor-ready before they land in your codebase or CI pipeline.
Updated
- #svg
- #optimization
- #svgo
- #currentcolor
- #icon-pipeline
- #vector
- #cleanup
Every icon starts its life as a designer's export. By the time an SVG leaves Figma, Illustrator, or Sketch and lands in your pull request, it can carry dozens of kilobytes of editor metadata, hundreds of invisible layers, transform stacks that no browser ever asked for, and hardcoded fill colors that will never respect your design system's theme. This guide walks through exactly what that cruft is, why it matters, and how to remove it reliably — manually, with tooling, or as part of an automated pipeline.
What "editor cruft" actually looks like
Open any freshly exported SVG from a major design tool and you will find at least some of the following:
<sodipodi:...>and<inkscape:...>namespace elements (Inkscape-specific metadata)<dc:creator>,<cc:Work>,<rdf:RDF>RDF/Dublin Core metadata blocks- Adobe-specific
xmp:CreatorToolandxmp:ModifyDateattributes embedded in<metadata> - Sketch
sketch:typeand Figmadata-nameattributes on groups and layers - Dozens of
id="path1234"attributes on every element, generated to satisfy the tool's internal graph — useless in production - Empty
<g>wrapper elements that group nothing - Invisible elements with
display:noneoropacity:0that the designer switched off but never deleted - The full document title in a
<title>element that will be read aloud by screen readers if you inline the SVG without overriding it
None of these survive the browser render in a way you can see, but they inflate file sizes, pollute the DOM, interfere with CSS targeting, and cause confusion when other developers read the source.
A before/after to make this concrete
A real-world icon exported from Figma (simplified for readability):
<!-- BEFORE: Figma export, 1.4 KB -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Settings</title>
<metadata>
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/">
<cc:Work><dc:creator>Figma</dc:creator></cc:Work>
</rdf:RDF>
</metadata>
<g id="Icons/Settings" data-name="Icons/Settings">
<g id="Frame" opacity="1">
<rect id="Bounding-Box" width="24" height="24" fill="transparent" display="none"/>
<path id="path-1" d="M12 15a3 3 0 100-6 3 3 0 000 6z"
fill="#1A1A1A" fill-rule="evenodd"/>
<path id="path-2" transform="translate(0,0) rotate(0) scale(1)"
d="M19.14 12.94..." fill="#1A1A1A"/>
</g>
</g>
</svg>
<!-- AFTER: cleaned, 290 bytes -->
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15a3 3 0 100-6 3 3 0 000 6z" fill="currentColor" fill-rule="evenodd"/>
<path d="M19.14 12.94..." fill="currentColor"/>
</svg>
That is an 80% size reduction. More importantly, the cleaned version is now themeable.
Flattening transforms into path data
Design tools often build icons through composition — a shape positioned via a group transform, then rotated, then scaled. When exported, those transforms survive as literal transform="translate(4,4) rotate(45) scale(0.5)" attributes. Browsers handle this correctly, but it creates three problems:
- Combining paths in CSS (e.g. with
clip-pathormask) requires coordinates to be in the same space. - Any post-processing tool that reads path coordinates (a QA check, a sprite builder, a bounds calculator) will get wrong numbers unless it also evaluates the transform chain.
- When you read the path data yourself to understand an icon's shape, you cannot visualize it without mentally applying the transforms.
Flattening means applying each transform to the d attribute of the path itself, then removing the transform attribute. The path data changes; the visual output is identical.
Most SVG optimizers do this automatically. In SVGO, the convertTransform plugin handles it. If you are applying transforms manually (rare but sometimes needed for complex rotation chains), use a library like svgpath to apply the matrix operations to the coordinate data.
Watch out: transform on a <g> with multiple children
When a transform sits on a <g> that wraps several paths, flattening requires applying the same matrix to each child path individually, then removing the <g> (or keeping it if it serves a semantic grouping purpose). Automated tools handle this, but if you are reviewing SVGs by hand, it is easy to miss a group-level transform.
Decimal precision
Figma and Illustrator export path coordinates to six or more decimal places by default: M 12.000000 4.499823. The visual difference between 12.000000 and 12 is zero for icon-sized artwork (anything under 512px). Excess decimal places are dead weight.
A safe rule for UI icons is two decimal places — enough to preserve sub-pixel precision at standard icon sizes while keeping path strings short. At 1px = 1 SVG unit and a 24×24 viewBox, two decimal places gives you precision to 0.01 pixels, which is meaningless at the sizes icons are actually displayed.
SVGO's cleanupNumericValues plugin applies this automatically. If you are writing your own cleaner, use a regex that rounds values in d attributes: this is a common but fiddly operation to get right around arc commands (A/a), so use a tested library rather than a hand-rolled regex.
Removing hidden layers and unused IDs
Hidden layers
The cleanup rule is simple: if an element has display:none, visibility:hidden, or opacity:0 and is not referenced by a <use>, <clipPath>, or <mask>, delete it. It contributes nothing to the output.
The "referenced" caveat is important. A shape can be invisible itself but still be used as a clip mask for a visible element. Check whether any id on the hidden element is referenced elsewhere before removing it.
Unused IDs
Every id in an inlined SVG pollutes the document's global ID namespace. If you inline two copies of the same icon (a pattern that appears constantly in component-based UIs), identical IDs create a validity error and can break CSS selectors and aria references.
The right approach:
- Remove IDs that exist only because the export tool generated them (
id="path1234",id="Group_47"). - Keep IDs that your CSS or JavaScript actually reference.
- In a component system (React, Vue, etc.), auto-generate unique IDs per instance for any
<clipPath>or<filter>that the SVG uses internally. Every major SVG component library handles this; if yours does not, it is a bug.
Making SVGs currentColor-ready
currentColor is the mechanism that lets an icon inherit its color from its surrounding text context — meaning a single SVG file works at any color, in any theme, without duplication. It is the baseline expectation for any icon intended for use in a design system.
The change is simple: replace every hardcoded color value on fill and stroke attributes with currentColor:
<!-- Before -->
<path fill="#111827" .../>
<path stroke="#374151" stroke-width="2" .../>
<!-- After -->
<path fill="currentColor" .../>
<path stroke="currentColor" stroke-width="2" .../>
Also check style attributes — some tools emit inline styles (style="fill:#111827") instead of presentation attributes. Both need to be converted.
Dual-tone icons
Some icons intentionally use two colors — a foreground shape and a secondary accent. In that case, a single currentColor is not enough. The conventional pattern is to use currentColor for the primary color and a --icon-accent CSS custom property for the secondary:
<path fill="currentColor" .../>
<path fill="var(--icon-accent, currentColor)" .../>
The currentColor fallback in var() means the icon degrades gracefully if the property is not set.
For more on how currentColor interacts with dark-mode variants and contrast requirements, see the guide on themeable icons and dark-mode variants.
The cleanup checklist
| Check | What to do | Automated? |
|---|---|---|
Editor metadata (<metadata>, RDF, XMP) |
Remove entirely | Yes (SVGO removeMetadata) |
Tool-specific namespaces (inkscape:, sodipodi:, sketch:) |
Remove namespace and all child elements | Yes (SVGO removeXMLNS, removeEditorsNSData) |
| Invisible elements | Remove if not referenced | Yes (SVGO removeHiddenElems) |
Identity transforms (translate(0,0), scale(1)) |
Remove | Yes (SVGO cleanupAttrs, convertTransform) |
| Non-identity transforms | Flatten into path data | Yes (SVGO convertTransform) |
| Unused IDs | Remove | Partly (SVGO cleanupIds; keep referenced IDs) |
| Hardcoded fill/stroke colors | Replace with currentColor |
Partly (SVGO convertColors + manual review) |
| Excess decimal places | Round to 2 dp | Yes (SVGO cleanupNumericValues) |
Empty <g> elements |
Remove | Yes (SVGO collapseGroups, removeEmptyContainers) |
<title> for decorative icons |
Remove or make empty + aria-hidden="true" |
Manual |
| Duplicate path data | Merge with <use> or deduplicate |
Manual / SVGO mergePaths |
Automated vs manual cleanup
When automation is enough
For a large icon library (hundreds of icons), manual review of every file is not realistic. A well-configured SVGO pass will catch everything in the checklist above automatically. Run it as part of your build pipeline so no raw exports ever land in the repository.
A minimal svgo.config.js for icon libraries:
// svgo.config.js
module.exports = {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
// Never remove viewBox — it controls scaling behavior
removeViewBox: false,
// Inline styles can be needed for multi-color icons; review case by case
inlineStyles: false,
},
},
},
'removeMetadata',
'removeEditorsNSData',
{
name: 'convertColors',
params: {
currentColor: true, // replaces any color matching the icon's body color
},
},
{
name: 'cleanupNumericValues',
params: { floatPrecision: 2 },
},
],
};
Run it with: npx svgo --config svgo.config.js --folder src/icons --output dist/icons
When you need a human eye
Automation cannot make judgment calls. You still need a person to:
- Confirm that
currentColorwas applied to the intended paths and not to accent elements that should stay a fixed color. - Spot icons where flattening transforms caused a path to land outside the viewBox (this happens with certain rotation/translation combinations and can result in clipped output).
- Catch icons where detail was lost because two nearly-overlapping paths were merged by
mergePathswhen they should have remained separate. - Validate that the cleaned icon still looks correct at small sizes (16px and below), where the relationship between strokes and fills matters more than it does at 48px.
A practical workflow is to run automated cleanup on every file, then do a visual QA pass on the output — ideally using a grid view that shows the icon at 16px, 24px, and 48px side by side.
Integrating cleanup into CI
The cleanest approach is to treat raw SVG exports as source files that are never committed to the main branch directly. Instead:
- Designers commit raw exports to a
src/icons/raw/directory. - A CI step (GitHub Actions, GitLab CI, etc.) runs SVGO against
src/icons/raw/and writes optimized output todist/icons/. - A second CI step runs your visual QA checks (pixel-diff against approved references, or a contrast/sizing lint).
- Only the
dist/icons/output is deployed or published to your npm package.
This keeps the build reproducible and ensures no unreviewed raw export ever ships. It also makes it easy to upgrade your SVGO configuration and regenerate the entire library in one pass.
For information on what happens after cleanup — specifically exporting your cleaned icons to framework-specific component formats — see the guide on exporting icons to React, Vue, SwiftUI, and WPF.