Authoring, tooling and optimising SVG files
Clean up design-tool output, configure SVGO for a real project, keep the viewBox honest, and decide between a font, a sprite and inline markup.
What a design tool actually exports
<!-- Typical editor output. Everything here is a problem. -->
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Some Tool 4.2.1 -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="24px" height="24px" viewBox="0 0 24 24" version="1.1" id="Layer_1"
xml:space="preserve" style="enable-background:new 0 0 24 24;">
<style type="text/css">
.st0{fill:#4A5568;}
.st1{fill:none;stroke:#4A5568;stroke-width:2;}
</style>
<g id="Group_7" transform="translate(0.5,0.5)">
<rect x="2" y="2" width="20" height="20" class="st0"/>
<path class="st1" d="M6 12h12"/>
</g>
</svg>
<!-- What you want to ship -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"
fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="2" y="2" width="20" height="20"/>
<path d="M6 12h12"/>
</svg>| Editor output | Problem | Fix |
|---|---|---|
| XML declaration | Meaningless in an inline SVG | Remove |
| Generator comment | Bytes and noise | Remove |
xml:space, version | Obsolete attributes | Remove |
width and height in px | Breaks fluid sizing | Keep only viewBox, or make the sizes 100% |
id on every group | Collides when inlined twice | Remove or prefix |
Internal <style> classes | Leaks into the page when inlined | Convert to presentation attributes |
Nested translate groups | Extra nodes, harder to edit | Bake into the coordinates |
Number precision like 12.34567 | Bytes with no visual difference | Round to 1-2 decimals |
⚠️
An internal
<style> block in an SVG is not scoped. Inline that file into a page and .st0 can match your HTML, and your page CSS can restyle the icon. Convert the classes to attributes, or give every class a prefix unique to the icon.SVGO with a deliberate configuration
npm install --save-dev svgo
# Inspect before you commit to a config
npx svgo --show-plugins
npx svgo --config svgo.config.mjs icon.svg -o icon.min.svg
# Batch a folder
npx svgo -f src/icons -o dist/icons --recursive// svgo.config.mjs — a configuration for icon files
export default {
multipass: true, // run the plugin chain until it settles
js2svg: { indent: 0, pretty: false },
plugins: [
{
name: 'preset-default',
params: {
overrides: {
// Keep ids: they are referenced by gradients, clipPaths and masks.
cleanupIds: false,
// Do not collapse groups blindly: some carry transforms you rely on.
collapseGroups: false,
// Round to two decimals: visually identical, fewer bytes.
cleanupNumericValues: { floatPrecision: 2 },
// Keep the viewBox. Without it the icon cannot scale.
removeViewBox: false,
// Convert shapes to paths only when it is safe.
convertShapeToPath: { convertArcs: true }
}
}
},
// Remove editor metadata
'removeXMLProcInst',
'removeComments',
'removeMetadata',
'removeEditorsNSData',
'removeDimensions', // drop width/height, keep viewBox
{
name: 'addAttributesToSVGElement',
params: {
attributes: [{ 'aria-hidden': 'true' }, { focusable: 'false' }]
}
}
]
};removeViewBoxmust stay off. Removing it produces a file that cannot be scaled, which is the single most common SVGO mistake.cleanupIdsrenames ids and can break references if a plugin is not aware of them. Keeping ids is safer for anything using gradients, masks or<use>.multipass: trueproduces smaller output than a single pass because optimizing a path can enable a later optimization.removeDimensionsdropswidthandheightso the icon fills its container — which is what you want for a sprite or a CSS-styled icon, and wrong for a fixed-size decorative image.
| File | Before | After SVGO |
|---|---|---|
| A single 24px icon | 1.2 KB | 0.4 KB |
| A detailed illustration | 82 KB | 34 KB |
| A traced bitmap logo | 240 KB | 180 KB |
| A chart exported from a tool | 45 KB | 21 KB |
| An icon with a gradient, ids kept | 1.8 KB | 0.9 KB |
Font, sprite or inline
| Method | Colour control | Caching | Per-icon cost | Fits |
|---|---|---|---|---|
| Icon font | color only | One cached file | One glyph, tens of bytes | Many similar monochrome icons |
| Inline SVG | Full: multi-colour, animation | Not cached separately | Full markup per use | Icons that must be styled or animated |
<img src> | None | Cached | A request, or one from a sprite | Decorative images |
| CSS background | None | Cached | One rule | Purely decorative marks |
External sprite with <use> | Limited: currentColor works | One cached file | A few bytes | A large icon set with some styling needs |
| SVG in a data URI | None | Cached with the CSS | ~33% base64 overhead | Very small, fixed marks |
<!-- The three shapes of the same decision -->
<!-- 1. Inline: the icon is styleable and animateable -->
<button class="btn">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false">
<path d="M12 5v14M5 12h14" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
Add item
</button>
<!-- 2. Image: fixed, decorative, cacheable -->
<img src="/icons/logo.svg" alt="" width="120" height="32">
<!-- 3. Sprite reference: one cached file, many icons -->
<svg width="16" height="16" aria-hidden="true">
<use href="/icons/sprite.svg#plus"></use>
</svg>The practical default for most projects: inline the handful of icons whose colour or size changes with context, put everything else in a sprite or an <img>, and skip icon fonts unless the icon set is large, monochrome and static.
FAQ
Why does my icon disappear after optimising?
Almost always a removed
viewBox or a broken id reference. Set removeViewBox: false and cleanupIds: false, then re-check the gradient, mask and <use> targets by name.Should icons be inline or in a sprite?
Inline when the icon must change colour with state, be animated, or be accessible with its own title. A sprite when the set is large and most icons are used in a single colour — the bytes saved per use are substantial even if the sprite itself is larger than any single icon.
Related
Symbol sprites, icon systems and reuse Shapes and coordinates
Last refreshed 2026-09-18.