ICON OOP
Icons & Logos
Guide

SVG sprites: build an icon sprite sheet

Paste the same icon into a page forty times and you have shipped it forty times. A sprite lets you define each icon once and draw it anywhere, in one line, still recolourable and still scalable. Here is how to build one properly, and how to know when you actually need it.

HomeGuidesSVG sprites

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

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 own viewBox, 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 by id and 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-trash in 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 requestNoneOne, on first load
Cached across pagesNo, re-sent with every pageYes, fetched once
CSS styling of iconsWorksWorks
Cross-originNot applicableBlocked unless CORS headers allow it
Best forSmall sets, above-the-fold icons, single-page sitesLarge 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:

MethodCSS recolouringCachedMarkup cost per useBest for
Inline SVGYesNoHigh, the whole shapeOne-off icons, hero graphics
Sprite + <use>YesYes, if externalLow, one lineRepeated UI icons
<img src="icon.svg">NoYesLowLogos, images that never change colour
CSS background-imageNoYesLowPure decoration
Icon fontYesYesLowLegacy 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:

SymptomLikely causeFix
Nothing renders at allSprite hidden with display:noneHide with position:absolute;width:0;height:0;overflow:hidden
Icon renders tiny or clippedMissing viewBox on the symbolAdd the original icon's viewBox to the symbol
Works locally, blank in productionExternal sprite on another originServe same-origin, or add CORS headers
Icon will not change colourHard-coded fill in the symbolReplace with currentColor
One icon blank, others fineid mismatch, ids are case sensitiveCheck 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

Yes, but for different reasons than in 2014. HTTP/2 removed the request-count penalty that first made sprites popular, so bundling icons no longer saves you much network time. What a sprite still buys you is a single definition per icon, one cached file, and clean markup: an icon used forty times appears once in the HTML source instead of forty times. On icon-heavy interfaces that is a real reduction in page weight and a real gain in maintainability.
There are four usual causes. The symbol has no viewBox, so it has no coordinate system and collapses. The outer sprite svg is hidden with display:none, which in some browsers stops the symbols resolving, so hide it with width 0, height 0, position absolute and overflow hidden instead. The id in the use reference does not match the symbol id exactly, and ids are case sensitive. Or the sprite is an external file being loaded cross-origin, which browsers block without the right CORS headers.
Yes, if the icon inherits colour rather than declaring it. Set fill or stroke to currentColor inside the symbol and remove every hard-coded hex value. Each use instance then takes the colour of its parent, so setting a CSS colour on the wrapper recolours that instance only. Icons with baked-in fill attributes cannot be recoloured this way, which is why cleaning the source SVG first matters.
A symbol is never rendered until it is referenced, and it can carry its own viewBox, so each icon keeps its own coordinate system and scales independently. A g is just a group: it renders wherever it sits and takes no viewBox, so every icon in the sprite would share one coordinate space and you would have to offset each one by hand. Use symbol.
Inline the sprite when the icon set is small, roughly under 5 KB, or when the icons appear above the fold and you want zero extra requests. Use an external file when the sprite is large or shared across many pages, because the browser caches it once and reuses it everywhere. The trade-off is that an external sprite costs one request on first load and must be served from the same origin, or with CORS headers.
Treat the outer svg exactly as you would any other icon. If the icon is decorative, add aria-hidden="true" and focusable="false" to the svg that contains the use element. If it carries meaning on its own, add role="img" and an aria-label to that same svg. The use element and the symbol need no ARIA attributes of their own.

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.

Build your sprite from clean icons

Search 23,000+ icons and logos, recolour, resize and copy optimised SVG ready to drop into a symbol.

Open the ICON OOP tool