Icon System

A themeable icon set built with <symbol> and <use>

Why a sprite?

An SVG sprite is a single hidden <svg> containing many <symbol> elements. Each symbol defines one icon's geometry along with its own viewBox. You then stamp out instances of any icon with <use href="#icon-id">. The benefits are concrete:

  • The path data for each icon is written exactly once, even if the icon appears dozens of times on the page.
  • Changing the icon's appearance — size, color, stroke width — is a CSS problem, not an HTML problem. You don't touch the sprite.
  • currentColor means the icon inherits the CSS color property from its parent, so theming is a one-line CSS change.

This approach is explored in Tutorial 08: Reuse and Tutorial 12: Styling SVG with CSS.

The sprite definition

The sprite sits at the top of <body> as an inline <svg aria-hidden="true" style="display:none">. It takes up zero space and is invisible to sighted users and screen readers alike. Each <symbol> has an id used for referencing and a viewBox that defines its internal coordinate system — so every icon scales correctly regardless of the size you render it at.

All paths use stroke="currentColor" and fill="none", which is the hallmark of a line-art icon set. This defers color entirely to CSS.

<!-- Hidden sprite — place at the top of <body> --> <svg aria-hidden="true" style="display:none" xmlns="http://www.w3.org/2000/svg"> <defs> <symbol id="icon-home" viewBox="0 0 24 24"> <path d="M3 12L12 3l9 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="M5 10v9a1 1 0 0 0 1 1h4v-4h4v4h4a1 1 0 0 0 1-1v-9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/> </symbol> <symbol id="icon-search" viewBox="0 0 24 24"> <circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" fill="none"/> <line x1="16.5" y1="16.5" x2="21" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> </symbol> <!-- …heart, star, settings defined similarly --> </defs> </svg>

Instancing icons with <use>

Each rendered icon is a small <svg role="img" aria-label="…"> wrapper containing a single <use>. The role="img" and aria-label make the icon accessible to screen readers even though the geometry lives in a hidden sprite. The width and height on the outer <svg> control the rendered size.

<!-- The icon grid --> <li class="icon-cell"> <svg role="img" aria-label="Home" width="40" height="40"> <use href="#icon-home"/> </svg> <span>Home</span> </li> <li class="icon-cell"> <svg role="img" aria-label="Search" width="40" height="40"> <use href="#icon-search"/> </svg> <span>Search</span> </li>

Theming with currentColor and a CSS custom property

The grid's ancestor element holds --icon-color and sets color: var(--icon-color). Because SVG's stroke="currentColor" resolves to the CSS color of the element, every icon in the grid adopts that color automatically. To switch the whole set to a new color, one line of JavaScript is all it takes:

#icon-demo-root { --icon-color: #1f8a4c; /* initial color */ color: var(--icon-color); } function setTheme(color) { document.getElementById('icon-demo-root') .style.setProperty('--icon-color', color); }

Use the theme buttons below to see how every icon repaints from one CSS change — the SVG markup never changes.

Live demo

Theme color:

  • Home
  • Search
  • Heart
  • Star
  • Settings

What to explore next

This pattern scales well: real-world icon libraries like Heroicons and Phosphor ship their icons as SVG sprites in exactly this format. You can extend the system by adding a stroke-width custom property for line thickness, or a fill variant for solid icons. For accessibility in complex UIs, Tutorial 15: Accessibility covers when to prefer <title> inside the SVG versus an aria-label on the wrapper element.