If you have ever opened a page's source and found the same 900-character chevron path repeated in every table row, you already understand the problem a sprite solves. Inline SVG is the best way to place a single icon. It is a terrible way to place the same icon two hundred times.
An SVG sprite fixes that. It is one file, or one hidden block of markup, that holds every icon your interface uses. Each icon is defined once. Everywhere you want to draw one, you write a short reference instead of the whole shape. The icon still scales, still inherits colour, and still animates. You just stop repeating yourself.
The short version
Wrap each icon in a <symbol> with a unique id and its own viewBox. Put all the symbols in one hidden <svg>. Draw an icon with <svg><use href="#icon-name"></use></svg>. Use currentColor inside the symbol so every instance can be recoloured with CSS.
- What an SVG sprite actually is
- Do you still need one in 2026?
- Building the sprite, step by step
- Drawing an icon with <use>
- Making sprite icons recolourable
- Inline sprite vs external sprite file
- Automating it in a build
- Sprite vs inline vs img
- Accessibility with sprites
- When the icon does not show up
- Frequently asked questions
What an SVG sprite actually is
The name is borrowed from the old CSS sprite technique, where you crammed dozens of PNG icons into one big image and used background-position to show the slice you wanted. That was a hack forced on us by slow, request-limited browsers, and it was miserable to maintain.
An SVG sprite is nothing like that. There is no grid, no coordinate maths, no offsets. It is simply a container SVG holding a set of <symbol> elements, each one a complete icon with its own name and its own viewBox. Nothing in the sprite renders until you point at it.
Two SVG elements do all the work, and it is worth knowing exactly what each one is for. The MDN reference for <symbol> puts it plainly: a symbol defines graphics that are never drawn directly, only when instantiated. And the <use> element is the instruction that draws one, cloning the referenced node into the current document.
<symbol>is the definition. It is invisible by default and it can carry its ownviewBox, which is the crucial part: each icon keeps its own coordinate system, so a 24 by 24 icon and a 48 by 48 logo can live in the same sprite without interfering with each other.<use>is the instance. It references a symbol byidand clones it into place at whatever size the parent<svg>is given.
People sometimes try to build a sprite out of <g> groups instead. Do not. A <g> renders where it sits, takes no viewBox, and forces all your icons into one shared coordinate space, which means manual offsets and a sprite that breaks the moment you add an icon of a different size.
Do you still need one in 2026?
An honest answer, because plenty of tutorials still sell sprites on a benefit that no longer exists.
The original argument was request count. Under HTTP/1.1, browsers opened only a handful of parallel connections per host, so twenty icon files meant twenty queued requests and a visibly slower page. Bundling them into one file was a genuine performance win. HTTP/2 and HTTP/3 multiplex requests over a single connection, so that penalty is largely gone. If someone tells you a sprite will make your site dramatically faster by cutting requests, they are quoting a 2013 blog post.
The reasons that do still hold up are these:
- Markup weight. A chevron path repeated in 200 table rows is 200 copies of that path in your HTML, sent uncompressed-by-shape on every page load. With a sprite it appears once, and each instance costs about 40 bytes. On icon-dense interfaces this is not a rounding error.
- One place to change things. Swap the definition of
#icon-trashin the sprite and every trash icon in the product updates. No find and replace across templates. - Caching. An external sprite is fetched once and reused across every page in the site, which inline SVG can never do.
- Readable templates.
<use href="#icon-search">tells a future developer what the icon is. A wall of bezier coordinates does not.
The counter-case is real too. If you use six icons on a marketing site, a sprite is over-engineering: inline them, as covered in how to use SVG icons, and move on. Sprites earn their keep in applications, dashboards, tables, admin panels, anywhere the same handful of icons appears over and over.
Building the sprite, step by step
Four steps. You can do this by hand for a small set, and you should do it by hand once so you understand what the build tool is doing later.
Step 1: clean each icon. Open the raw SVG and strip it back. Remove editor metadata, remove fixed width and height, and remove hard-coded fill values, which will otherwise make the icon impossible to recolour. Keep the viewBox; it is the one attribute the sprite cannot work without. Running the file through SVGO handles most of this automatically, and our guide on optimising SVG files covers the settings that matter.
Step 2: wrap each icon in a symbol. Take everything inside the original <svg> tag, the paths and circles and rects, and drop it into a <symbol>. Give the symbol a unique, predictable id and copy the icon's viewBox onto it:
<symbol id="icon-search" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="7"/>
<path d="M20 20l-3.5-3.5"/>
</symbol>
Pick a naming convention now and stick to it. icon-search, icon-trash, icon-chevron-down. Ids are case sensitive and a typo produces a silent blank space, so consistency saves you real debugging time.
Step 3: collect the symbols into one container. All the symbols go inside a single outer <svg>, which must not render anything itself:
<svg xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0;overflow:hidden" aria-hidden="true">
<symbol id="icon-search" viewBox="0 0 24 24">...</symbol>
<symbol id="icon-trash" viewBox="0 0 24 24">...</symbol>
</svg>
Note what is not there: display:none. That is the single most common way to break a sprite. Some rendering engines treat a display-none SVG as removed from the render tree entirely, and the symbols inside stop resolving. Hide it with zero dimensions and overflow:hidden instead, which keeps it in the tree but takes it out of the layout.
Step 4: put the sprite on the page. Either paste that block immediately after the opening <body> tag, or save it as sprite.svg and reference it from an external URL. The two approaches behave differently in ways worth understanding, so they get their own section below.
Drawing an icon with <use>
Once the sprite exists, every icon in your templates looks like this:
<svg class="icon" width="24" height="24" aria-hidden="true" focusable="false">
<use href="#icon-search"></use>
</svg>
Three things are happening. The outer <svg> is the box: it decides the rendered size, takes your CSS class, and carries the accessibility attributes. The <use> pulls in the shape. The symbol's own viewBox scales the artwork to fill that box, which is why you can render the same symbol at 16 pixels in a table and 48 pixels in an empty state with no extra work.
One legacy detail: older tutorials use xlink:href instead of href. The xlink namespace is deprecated in SVG 2 and plain href works in every browser in current use. If you are supporting genuinely ancient clients you can include both attributes, but for a 2026 build, plain href is correct and cleaner.
Because the outer SVG controls the size, you can also let CSS drive it and stop writing width and height in markup at all:
.icon { width: 1.25em; height: 1.25em; vertical-align: -0.125em; }
Sizing icons in em makes them scale with their surrounding text, which is usually what you want in buttons and body copy. Our guide to icon sizes and grids goes into where each pixel size belongs and why the 24 pixel grid became the default.
Making sprite icons recolourable
This is the part that trips people up, and it comes down to one keyword.
Inside the symbol, do not declare a colour. Declare currentColor:
<symbol id="icon-heart" viewBox="0 0 24 24">
<path d="M12 21s-7-4.5-7-9a4 4 0 0 1 7-2.6A4 4 0 0 1 19 12c0 4.5-7 9-7 9z"
fill="none" stroke="currentColor" stroke-width="2"/>
</symbol>
currentColor is a CSS keyword meaning "whatever the computed value of color is here". Because <use> clones the symbol into the position of the instance, each cloned copy inherits the colour of its own parent. So this works exactly as you would hope:
.icon-danger { color: #E5484D; }
.icon-muted { color: #8B8B85; }
The same symbol, drawn twice, comes out red in one place and grey in the other. No duplicated definitions, no separate files per colour. If any hard-coded fill="#000000" survives in the symbol, that instance will stay black no matter what CSS you throw at it, which is why the cleaning step is not optional. The guide to changing icon colour covers the fill versus stroke distinction in more depth, and every icon you download from ICON OOP is already set up to inherit colour this way.
There is one honest limitation. Multi-colour icons, brand logos in particular, cannot be recoloured with a single color value, because they have several fills. You can reach into them with CSS custom properties defined on the instance, but for full-colour brand logos you are usually better off leaving them alone: their colours are the point.
Inline sprite vs external sprite file
Both are valid. They fail in different ways, so pick deliberately.
| Inline sprite (in the HTML) | External sprite (sprite.svg) | |
|---|---|---|
| Extra HTTP request | None | One, on first load |
| Cached across pages | No, re-sent with every page | Yes, fetched once |
| CSS styling of icons | Works | Works |
| Cross-origin | Not applicable | Blocked unless CORS headers allow it |
| Best for | Small sets, above-the-fold icons, single-page sites | Large sets, multi-page sites, apps |
The external version is referenced by path plus fragment:
<use href="/assets/sprite.svg#icon-search"></use>
Two rules keep external sprites out of trouble. First, same origin. If you serve the sprite from a different domain, including a CDN on another host, the browser will refuse to load it unless that host sends the appropriate CORS headers. This is the single most common reason an external sprite works locally and silently fails in production. Second, cache it hard. A sprite is a perfect candidate for a fingerprinted filename and a long Cache-Control lifetime, which is exactly the pattern web.dev's caching guidance recommends for static assets.
A reasonable hybrid, if you care about first paint: inline the six or seven icons that appear in the header and above the fold, and put the long tail in an external sprite that loads with everything else.
Automating it in a build
Hand-assembling a sprite is fine for twenty icons and unbearable for two hundred. Every ecosystem has a tool that will take a folder of SVGs and emit a symbol sprite:
- Node, framework-agnostic. svg-sprite is the long-standing option and will emit symbol sprites among several other modes.
- Vite and Rollup. Plugins in this space generally take a directory, run SVGO over it, and inject a symbol sprite into the app shell, so adding an icon means dropping a file into a folder.
- Anything else. A twenty-line script that reads a folder, strips the outer
<svg>tags, wraps each file in a<symbol>using the filename as the id, and concatenates the lot, will get you there. This is genuinely not a hard problem, which is why so many teams end up with their own.
Whatever you use, run SVGO first and make sure the viewBox is preserved. Several SVGO presets will happily remove the viewBox when width and height are present, and that will quietly destroy your sprite.
Save yourself the pipeline
If you only need a handful of icons, you do not need a build step at all. Search the ICON OOP library, copy each icon's clean SVG, and paste the paths straight into your symbols. Every icon is already stripped of metadata and already uses currentColor, so it drops into a sprite without editing.
Sprite vs inline vs img
There are five ways to get an SVG icon onto a page and none of them is universally right. Here is where the sprite sits among them:
| Method | CSS recolouring | Cached | Markup cost per use | Best for |
|---|---|---|---|---|
| Inline SVG | Yes | No | High, the whole shape | One-off icons, hero graphics |
Sprite + <use> | Yes | Yes, if external | Low, one line | Repeated UI icons |
<img src="icon.svg"> | No | Yes | Low | Logos, images that never change colour |
CSS background-image | No | Yes | Low | Pure decoration |
| Icon font | Yes | Yes | Low | Legacy systems only |
The pattern is clear enough: if an icon needs to change colour and appears repeatedly, the sprite is the only method that gives you both without bloating the page. If you are still weighing up the alternatives, SVG vs icon fonts covers why the last row is a legacy row, and SVG vs PNG covers the format question underneath all of this.
Accessibility with sprites
Nothing special happens here, which is the good news. The <use> element and the <symbol> both stay out of it. All the accessibility attributes go on the outer <svg> that wraps the <use>, exactly as they would on a normal inline icon.
Decorative icon, sitting beside text that already says what it means:
<svg class="icon" aria-hidden="true" focusable="false">
<use href="#icon-mail"></use>
</svg> Email us
Meaningful icon, carrying information on its own:
<svg class="icon" role="img" aria-label="Verified">
<use href="#icon-check"></use>
</svg>
And for an icon-only button, the label goes on the button and the SVG is hidden. Do not put a <title> inside the symbol and then also label the button, or the name gets announced twice. The full set of rules, including contrast and tap targets, is in the accessible icons guide, and the WCAG guidance on non-text contrast is the reference for the 3 to 1 ratio that meaningful icons must clear.
When the icon does not show up
A sprite fails silently. You get an empty box, no console error, no clue. In practice it is almost always one of these five:
| Symptom | Likely cause | Fix |
|---|---|---|
| Nothing renders at all | Sprite hidden with display:none | Hide with position:absolute;width:0;height:0;overflow:hidden |
| Icon renders tiny or clipped | Missing viewBox on the symbol | Add the original icon's viewBox to the symbol |
| Works locally, blank in production | External sprite on another origin | Serve same-origin, or add CORS headers |
| Icon will not change colour | Hard-coded fill in the symbol | Replace with currentColor |
| One icon blank, others fine | id mismatch, ids are case sensitive | Check the exact string in the use reference |
Check them in that order and you will find the fault in under a minute. If everything renders but the file feels heavy, the problem is upstream in the icons themselves, not the sprite, and SVG optimisation is where to look.
Frequently asked questions
Next steps: clean your icons with SVGO before they go into the sprite, get the sizes and stroke weights right, check the accessibility rules, or browse 23,000+ icons that are already sprite-ready.