SVGO settings that are safe for icons
A practical SVGO setup for an icon library that keeps the viewBox, prefixes IDs, picks a sensible precision and runs the same way in every build.
5 min read
SVGO is a Node.js tool that rewrites SVG files to be smaller without changing how they look. Its defaults are tuned for single images. An icon library has different needs: hundreds or thousands of files, many of them inlined side by side, all expected to scale to any size. A few settings make the difference between a clean set and one with cropped icons and swapped gradients.
This guide assumes a current SVGO release. Plugin names and the contents of the default preset have changed between major versions, so keep svgo.dev open for the version in your lockfile.
Running it once
Add SVGO as a development dependency and try it on a single file:
npm install --save-dev svgo
npx svgo src/icons/coin.svg -o /tmp/coin.svg
SVGO prints the size before and after. Open both files in a browser. If they look identical, you have a baseline. Most icon files shrink noticeably on the first pass, mostly from editor metadata, long decimals and redundant groups.
What the default preset does
With no configuration, SVGO runs a bundle of plugins called preset-default. Broadly, it:
- removes comments, metadata, editor namespaces and the XML declaration;
- removes hidden elements, empty containers and unused definitions;
- shortens colors, numbers and path data;
- collapses unnecessary groups and merges compatible paths;
- converts simple shapes to paths when that is shorter;
- shortens IDs (the
cleanupIdsplugin) and removes unreferenced ones.
Nearly all of that is what you want. Three things need attention for icons: the viewBox, the shortened IDs, and precision.
Keep the viewBox
Some SVGO versions include a plugin called removeViewBox in the default preset. It drops the viewBox when it matches the width and height, on the theory that it is redundant. For icons it is the opposite of redundant: without it, an icon resized by CSS no longer scales its drawing.
Check the preset-default page on svgo.dev for your version. If removeViewBox is listed, turn it off explicitly. If it is not listed, the override is unnecessary; delete it (SVGO may warn when you override a plugin that the preset does not contain).
After any run, confirm that every file kept its viewBox:
grep -L 'viewBox' public/icons/*.svg
grep -L lists files that do not contain the text, so empty output is what you want. The viewBox guide explains what breaks when it is missing.
Prefix IDs so icons can share a page
cleanupIds renames IDs to the shortest available names: a, b, c. Each file is processed on its own, so the first gradient in every logo becomes a. That is fine for files shown through <img>, which each get their own document. It is a problem once you inline two of them into one HTML page, because both now define id="a" and the second logo's fill="url(#a)" points at the first logo's gradient. The full story is in SVG ID collisions.
The prefixIds plugin fixes this by adding a per-file prefix to every ID and class name, and updating the references to match. By default it derives the prefix from the file name, so the a in eth.svg becomes something like eth_svg__a. Order matters: list prefixIds after preset-default, so it prefixes the already shortened IDs.
Prefixing makes IDs unique across files, not across repeated copies of the same file. If the same inline icon appears twice on a page, both copies still share IDs. That is usually harmless, but it breaks if the first copy is hidden with display: none. For components rendered many times, generate IDs at runtime instead.
Choose precision from the viewBox
SVGO rounds numbers. The right number of decimals depends on how many units the viewBox spans, because rounding error is measured in units and the visible effect depends on how large each unit is on screen.
| viewBox width | Decimal places | Worst-case error at 256 px |
|---|---|---|
| 16 to 48 units | 2 | 0.03 to 0.08 px |
| 100 to 500 units | 1 | 0.03 to 0.13 px |
| 1000 units and up | 0 | 0.13 px or less |
All of those are well under a pixel. Going one step lower (one decimal in a 24-unit icon) is often still fine, but small curves can begin to wobble, so check visually before committing to it.
Precision is set per plugin. The two that matter most are convertPathData, which handles path coordinates, and cleanupNumericValues, which handles other attributes. The command line also accepts -p (or --precision) for quick experiments.
A configuration for an icon library
SVGO picks up svgo.config.mjs from the directory you run it in:
// svgo.config.mjs
export default {
multipass: true,
plugins: [
{
name: 'preset-default',
params: {
overrides: {
// Keep only if your SVGO version's preset-default includes removeViewBox.
removeViewBox: false,
convertPathData: { floatPrecision: 2 },
cleanupNumericValues: { floatPrecision: 2 },
},
},
},
'prefixIds',
],
}
multipass: true runs the plugins repeatedly until a pass makes no further change. It is a little slower and occasionally saves a few more bytes.
Plugins outside the preset are worth knowing about, but only add them for a specific reason:
| Plugin | Use it when | Watch out for |
|---|---|---|
removeDimensions |
You always size icons with CSS | Files lose their default size in <img> without attributes |
removeXMLNS |
Output is only ever inlined in HTML | The files stop working as standalone images |
convertColors with currentColor |
Building a mono set that should follow text color | Never on multi-color logos |
removeAttrs |
Stripping a specific attribute from every file | Easy to remove something important, such as fill from a branded logo |
If your files carry accessibility attributes such as role, check the options of removeUnknownsAndDefaults on svgo.dev; it has settings for which of those it keeps.
Run it as a build step
Treat optimized files as build output, generated from source files you keep untouched:
npx svgo --recursive --folder src/icons --output public/icons
Put that command in a package.json script so everyone runs it the same way, and run the same script in CI. Two habits keep the set healthy:
- Never optimize in place on your only copy. Optimization discards layer names and editable shapes. Keep the originals in
src/iconsor wherever your source lives. - Fail the build on drift. If optimized files are committed, have CI regenerate them and fail when the result differs from what is in the repository. That catches files someone dropped in without running the script.
When something looks wrong
If an icon changes after optimization, rerun SVGO on that file with the suspect plugin disabled in overrides (for example mergePaths: false or collapseGroups: false) until the output matches. Fix the one file or keep the override for the whole set, depending on how often it happens.
The checks in optimizing SVG files without breaking them (a difference overlay, several display sizes, light and dark backgrounds) apply to every config change, not just the first run.