An SVG sprite puts every icon into one file as a <symbol>, and each icon on the page is a short <use> reference to it. It's one of the oldest ways to ship icons, and in React codebases it's often dismissed in favour of one component per icon. This note covers when the sprite is the better choice, what the markup looks like, and the three gotchas that make people give up on it.
Key takeaways
- A sprite keeps icon markup out of your JavaScript bundle; components put every icon you import into it.
- Usehref, notxlink:href, and serve the sprite from the same origin as the page.
- Colour sprite icons withcurrentColor: your CSS selectors don't reach inside a<use>, but inherited properties do.
- Pick the sprite for many icons per page or non-React consumers; pick components when each icon needs its own behaviour.
What a sprite looks like
A sprite for three icons, with colours already mapped to currentColor, looks like this (an illustrative, trimmed example):
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id="icon-arrow-right" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2"/></symbol>
<symbol id="icon-check-circle" viewBox="0 0 24 24">…</symbol>
<symbol id="icon-github" viewBox="0 0 16 16">…</symbol>
</svg>A <symbol> is a template: "not meant to be rendered", only its instances are (MDN: symbol). Each one keeps its own viewBox, so a 16-pixel logo and a 24-pixel arrow can share a file. The prefix, icon- here, keeps the IDs from colliding with anything else on the page.
How to use it
An icon on the page references a symbol by ID:
<svg width="24" height="24" aria-hidden="true">
<use href="/icons-sprite.svg#icon-arrow-right" />
</svg>In React, one small component covers every icon:
type IconName = "arrow-right" | "check-circle" | "github";
export function Icon({ name, label }: { name: IconName; label?: string }) {
return (
<svg
width="1em"
height="1em"
role={label ? "img" : undefined}
aria-label={label}
aria-hidden={label ? undefined : true}
>
<use href={`/icons-sprite.svg#icon-${name}`} />
</svg>
);
}1em makes the icon follow the text size. The union type turns a typo in an icon name into a type error instead of an invisible icon. Generate it from the same list as the sprite so the two can't drift apart.
Sprite or components
When the sprite wins
Many icons on a page. With components, every icon you import becomes JavaScript: path data in the bundle, parsed and executed before it renders. And every instance on the page renders its full path markup, while a sprite instance is a single <use> element. The JavaScript cost matters because, in a line Groß quotes from web.dev on kurtextrem.de, "Byte-for-byte, JavaScript is more expensive for the browser to process than the equivalently sized image or Web Font" (Breaking Up with SVG-in-JS).
Consumers that aren't React. A sprite works in a server-rendered template or a static page without a build step. The same file serves all of them.
A stable, shared set. A set that rarely changes stays one long-cached file. The Epic Stack, a full-stack React starter, made this choice: it "uses SVG sprites for optimal icon performance" (Epic Stack docs: icons).
When components win
- Per-icon behaviour. An animated icon, or one with a part that changes on hover, needs its inner elements in the page's DOM.
- A handful of icons. If a page uses three icons, tree-shaken components cost little and avoid a second request.
- Multi-colour icons. They work in a sprite, but you control individual fills only through CSS custom properties set inside the symbol, such as
fill="var(--icon-accent)".
Three gotchas
1. xlink:href is deprecated
"Since SVG 2, the xlink:href attribute is deprecated in favor of href" (MDN: use). Old snippets still use it. Write href; if both are present, href wins.
2. Serve the sprite from your own origin
MDN is careful here: browsers "may apply the same-origin policy on <use> elements and may refuse to load a cross-origin URL", and "there is currently no defined way to set a cross-origin policy for <use> elements". There's no CORS opt-in, so a sprite on a CDN domain may simply not load, with no error in the page. Serve it from the page's origin, or inline the sprite into the page once.
3. Selectors don't reach inside
Your CSS selectors don't match elements inside the <use> copy; only inherited properties reach them. MDN puts it this way: properties "are not guaranteed to be inherited by the cloned DOM unless you explicitly request them using CSS inheritance". A rule like .icon path { stroke: red } won't work. Draw icons with currentColor and set color on the outer <svg>: color is inherited, so the icon follows it.
Figma exports the colour the designer drew with, so the mapping to currentColor happens in your build. Our icons from Figma to React guide shows that replacement step for components.
Accessibility
Decorative icons next to a text label get aria-hidden="true", which removes them from the accessibility tree (MDN: aria-hidden). An icon that carries meaning on its own, such as an icon-only button, gets role="img" and an aria-label on the outer <svg> (MDN: img role). The Icon component above does both from one prop.
Getting the sprite out of Figma
Generating a sprite is the easy part; keeping it current is the hard part. Every icon added, edited or renamed in Figma has to reach the file, with a diff someone can review, the same review-by-pull-request flow we describe for design tokens from Figma to code.
We build FISYCO, and it can deliver a Figma icon library as a sprite: one <symbol> per icon, a prefix you choose, and a reviewable pull request when icons change. Today it delivers sprite icons with the colours as drawn in Figma; colour replacement applies to components only. To recolour sprite icons from CSS, add a colour-mapping step to your own build for now. Need both formats? Connect the same Figma file twice: once as components for one repository, once as a sprite for another.



