Topics

SVG sprites

A sprite is one SVG file with a symbol per icon. Pages reference the symbols with <use>, the browser downloads and caches the file once, and no JavaScript runs at all.

Get the sprite

The package ships a sprite with every icon at glyphfox/sprite.svg. Each icon is a <symbol> with the id gf- plus the icon name, its own viewBox and the stroke settings of the set.

terminal
npm install glyphfox
cp node_modules/glyphfox/dist/sprite.svg public/icons/sprite.svg
sprite.svg
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="gf-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">...</symbol>
<symbol id="gf-search" viewBox="0 0 24 24" ...>...</symbol>
...
</svg>

With a bundler you can let it copy and fingerprint the file instead. In Vite, the ?url suffix returns the final URL of the asset:

icons.js
import spriteUrl from 'glyphfox/sprite.svg?url';

export const icon = (name) =>
  `<svg class="icon" width="24" height="24" aria-hidden="true"><use href="${spriteUrl}#gf-${name}"></use></svg>`;

The full sprite holds all 419 icons in 117.9 KB, or 15.7 KB with gzip. A sprite with the 30 icons a typical app shell uses is 7.5 KB, 1.3 KB with gzip; the CLI builds one for you, as shown below.

Reference a symbol

Write an outer svg with the size you want and a use element that points at the symbol. The symbol brings its own viewBox, so the outer element needs only width and height.

index.html
<button type="button" class="btn">
  <svg class="icon" width="20" height="20" aria-hidden="true">
    <use href="/icons/sprite.svg#gf-download"></use>
  </svg>
  Download
</button>
app.css
.icon {
  width: 1.25em;
  height: 1.25em;
  flex-shrink: 0;
  vertical-align: -0.2em;
}
.btn:hover .icon {
  color: #ff7a1a;
}

The symbols draw with stroke="currentColor", and color inherits into the use content, so the icon follows the text color of its parent and any color you set on the outer svg. Width and height in CSS override the attributes, which lets the size follow the font size.

Same origin only

Browsers only follow a use reference to a file on the same origin as the page. A sprite on a CDN such as jsDelivr, or on another subdomain of your site, renders nothing, and CORS headers do not change that. Serve the sprite from your own origin, or fetch it once and put it in the page:

inline-sprite.js
// jsDelivr sends CORS headers, so fetch works where <use> across origins does not.
const res = await fetch('https://cdn.jsdelivr.net/npm/glyphfox@1/dist/sprite.svg');
const holder = document.createElement('div');
holder.setAttribute('aria-hidden', 'true');
holder.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden';
holder.innerHTML = await res.text();
document.body.prepend(holder);

// now <use href="#gf-home"></use> works anywhere on the page

Inline the sprite in the page

An inline sprite is the same symbols placed once in the HTML, usually right after <body>. References then use only the fragment, #gf-home, there is no extra request and the icons render with the first paint. The cost is that every page carries the symbols again, so inline a small subset rather than the full set.

layout.html
<body>
  <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true"
       style="position:absolute;width:0;height:0;overflow:hidden">
    <symbol id="gf-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">...</symbol>
    <symbol id="gf-search" viewBox="0 0 24 24" ...>...</symbol>
  </svg>

  <a href="/"><svg class="icon" aria-hidden="true"><use href="#gf-home"></use></svg> Home</a>
</body>

Hide the sprite with zero size and absolute positioning, not with display:none. Some browsers stop rendering masks that live inside a hidden SVG, and the solid style uses masks to cut its inner lines.

Build a subset with the CLI

glyphfox build writes a sprite with only the icons you list, rendered with the props you pass. Names can come from the command line or from a file with one name per line; spaces, commas and # comments are allowed in the file.

terminal
npx glyphfox build home search settings bell user --type sprite --out public/icons
npx glyphfox build --from icons.txt --type sprite --weight 1.5 --out public/icons
npx glyphfox build --from icons.txt --type sprite --variant solid --out public/icons/solid
icons.txt
# app shell
home, search, settings, bell, user
menu x chevron-down

# badges and fusions work too
file+check
cloud&lock:overlap

Plain names keep their id: home becomes gf-home. Specs with a badge or a fusion get an id with the symbols replaced: file+check becomes gf-file_check and cloud&lock:overlap becomes gf-cloud__lock-overlap. Open the generated file once to copy the ids you need.

Building at the size you display also gets you the optical compensation of the components: --size 16 writes the slightly heavier stroke that glyphfox uses for 16 pixel icons, so a sprite for dense tables looks as strong as the rest of the interface.

The same command with --type css writes a stylesheet of mask classes instead (<span class="gf gf-home">), for sites that want icon classes without Tailwind CSS. The Tailwind CSS guide explains how mask icons behave.

Build options

A sprite is static, so the shared props become build flags. These are the ones that matter for a sprite; every flag has the same meaning as the prop with that name in the props table.

FlagEffect on the symbols
--type spritewrites sprite.svg (the default type); css and package are the other types
--out DIRoutput directory, the current directory by default
--from FILEreads names from a file
--weight Nstroke weight from 1 to 3, written into each symbol
--size Nthe pixel size you display, for the optical stroke compensation below 24
--corners sharpsharp joins and caps
--variant duotone, --variant solidthe style of every symbol
--color C, --secondary-color Cfixed colors instead of currentColor
--secondary-opacity Nopacity of the duotone layer or the solid fill
--rotate DEG, --flip-x, --flip-ybakes a rotation or a mirror into the symbols

What CSS can and cannot change

What use draws is a copy of the symbol. CSS on the outer svg or on the use element reaches that copy only through inheritance, which is why color works. Properties that the symbol sets itself do not: each symbol carries stroke-width, stroke-linecap and stroke-linejoin as attributes, and those win over any value inherited from outside. Setting stroke-width on .icon has no effect.

Decide weight, corners and style when you build the sprite, and build two sprites if you need two weights. When you need per instance control over weight, a second color or motion, render inline SVG with a component or toSvg instead.

Whole icon animations still work, because they act on the outer element: .icon.spin { animation: spin 1s linear infinite } rotates a sprite icon like any other element.

Accessibility

A sprite icon is an outer svg that you write, so its semantics are yours to set. Hide decorative icons with aria-hidden="true". For an icon that carries meaning on its own, make the outer element an image with a name:

status.html
<svg class="icon" role="img" aria-label="Payment failed">
  <use href="/icons/sprite.svg#gf-circle-alert"></use>
</svg>

Icon only buttons need the label on the button, as in the accessibility guide.

Caching

An external sprite is one request that every page shares. Serve it with a long cache lifetime and a fingerprint in the file name, such as sprite.3f9a1c.svg, so a new build changes the URL instead of waiting for caches to expire. Bundlers do this for you when you import the file. Each distinct sprite URL is downloaded once per page, no matter how many icons reference it.

Pitfalls

A sprite on another origin
An external use pointing at a CDN or another subdomain renders an empty box; the browser only reports the blocked request in the console. Host the file on the page origin or inline it.
Duotone layers in the full sprite
The 42 icons that have a hand drawn duotone layer keep it in glyphfox/sprite.svg, as a translucent fill in the current color. If you want pure outlines, build a subset: the default outline style leaves the layer out, and --variant duotone keeps it.
Masks in hidden or external sprites
Solid symbols use a mask to cut their inner lines. Masks can fail to render when the sprite is hidden with display:none, and support for masks inside an external sprite file differs between browsers. Test the solid style in your target browsers, or inline that sprite.
Living states in a sprite
Presets such as spin or draw are scoped to a class on the root svg, which a symbol does not keep, so --animate has no visible effect in a sprite. Animate the outer element with your own CSS, or use inline SVG.
Ids that clash
An inline sprite puts ids such as gf-home into the page. Two inline sprites with the same icon, or your own element with the same id, make references ambiguous. Inline one sprite per page.

Questions

When is a sprite better than components?

On server rendered or static sites without a JavaScript framework, and on pages that repeat many icons: the symbols are downloaded once and every use is a few bytes of markup. Components win when icons change with state, morph, animate or need a second color, because a sprite is fixed at build time.

Can I reference the sprite on jsDelivr directly?

Not with use, because browsers block external references to other origins. Fetch the file and insert it into the page, as shown above, or copy it to your own server.

Icons in this guide