Working with SVG icons

There have been a number of ways to work with icons on the web throughout the years. A long, long time ago, when phones flipped open and everyone was watching Lost, many websites used sprite sheets that mapped many graphical elements to one location in a larger image file. Having many elements share one file limited the number of http requests over the wire, which sped up page load times.

Around 2010 or so, sprite sheets started to fade in popularity and icon fonts became popular. They loaded in one file and used SVGs under the covers. Icon fonts allowed web graphics to scale, were lightweight, and were easy to style using CSS. Espalier does not use icon fonts. So, if icon fonts were so cool, why doesn't Espalier use them?

It turns out icon fonts are very bad for accessibility. Also, modern SVG files provide all the benefits of icon fonts without any of the drawbacks. SVG files can hold many distinct graphical elements. The key is to include them as symbols with an id, then reference them by id when they are used.

How does one go about creating an SVG sprite sheet?

Create an SVG file

This SVG file will hold many SVG icons. They will be added to the <defs> node.

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg">
  <defs>
  </defs>
</svg>

Add SVG symbols

There are many resources for free and open source icons. Espalier has been using tabler icons. To add an icon to the SVG file:

  1. Go to tabler and click on an icon
  2. Click Copy SVG to copy the raw SVG
  3. Paste it into the defs section of the SVG file
  4. Rename the new svg node to symbol
  5. Delete the xmlns, width, height, and class attributes from the new symbol
  6. Give the new symbol an id attribute (maybe matching the tabler name)
  7. Make sure the stroke is set to currentColor so it will adopt the foreground color of the Espalier element it is placed in

The SVG file now looks like this:

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg">
  <defs>
    <symbol id="brightness-2" viewBox="0 0 24 24" fill="none"
      stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round">
        <path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M6 6h3.5l2.5 -2.5l2.5 2.5h3.5v3.5l2.5 2.5l-2.5 2.5v3.5h-3.5l-2.5 2.5l-2.5 -2.5h-3.5v-3.5l-2.5 -2.5l2.5 -2.5z" />
    </symbol>
  </defs>
</svg>

Using the SVG

For Espalier components that own a single icon position, prefer passing the symbol id through the component's icon attribute:

<esp-root icon-sprite-url="/assets/icons.svg">
  <esp-button label="Profile" icon="user-circle"></esp-button>
  <esp-badge intent="success" icon="circle-dot">Published</esp-badge>
</esp-root>

The icon-sprite-url attribute defaults to /assets/icons.svg. Set it when your site serves the sprite from a subpath, CDN, or hashed asset pipeline:

<esp-root icon-sprite-url="/dist/icons.abc123.svg">
  <esp-button label="Save" icon="save"></esp-button>
</esp-root>

Icon names are treated as symbol ids, not complete URLs. They must start with a letter or number and may contain letters, numbers, dashes, and underscores. Invalid names are ignored and logged once. Set icon-sprite-url="" when a root should disable generated sprite icons.

When you need to inline a custom SVG, avoid another asset request, or provide markup that is not part of the shared sprite, use the component's icon slot. Slotted SVGs override the icon attribute where both are present. Slotted SVG markup is used verbatim, including any <use href>, so use the icon attribute when the reference should follow icon-sprite-url.

You can still reference the SVG directly anywhere you own the markup. Use the URL#id form:

<svg width="100" height="100">
  <use href="/assets/icons.svg#brightness-2" />
</svg>

Fully offline pages

A self-contained deliverable — an email attachment, a single-file report, a page on a USB stick — cannot fetch /assets/icons.svg, and browsers block data: URLs as <use> targets. For those pages the sprite ships as an importable string, and icon-sprite-url accepts a same-document form: a value that is just a fragment, naming an inline sprite by id.

import { installIconSprite } from "@taprootio/espalier/icons";

// Inline the sprite once (re-runs are no-ops), then point the root at it.
document.querySelector("esp-root")
  .setAttribute("icon-sprite-url", installIconSprite()); // "#esp-icons"

Prefer inlining the sprite before setting the attribute (or set it again after): components adopt symbols when they render, and a missing sprite logs one warning until it appears. For a strictly zero-request page, put the same-document value in the root's markup (icon-sprite-url="#esp-icons") and install the sprite before the component modules load — a root that renders even once on the default URL fetches it. If you'd rather paste markup than run a helper, ICON_SPRITE is the raw string and ICON_SPRITE_ID the id its root <svg> carries — drop it at the end of <body> verbatim.

Every component then renders its icon attribute with zero network requests. Under the hood, a shadow tree cannot <use> a light-DOM id, so each component adopts exactly the symbols it references into a hidden container in its own shadow root — per symbol, on demand, never the whole sprite. Composition travels along: a symbol that builds on another via <use href="#part">, or paints through a gradient, filter, or mask via url(#resource), brings those dependencies with it, as long as they live in the same sprite. Symbols must stay passive SVG — shapes, paths, local composition; anything carrying scripts, styles, links, animation, event handlers, or external references is refused whole with a console warning.

Your own sprite

The same-document form is not tied to Espalier's sprite. Inline any sprite of your own making (the format from the top of this guide), give the root <svg> an id, and reference it:

<svg id="brand-icons" aria-hidden="true" style="display: none"
  xmlns="http://www.w3.org/2000/svg">
  <defs>
    <symbol id="wave" viewBox="0 0 24 24" fill="none" stroke="currentColor"
      stroke-width="2"><path d="M3 12c2-3 4-3 6 0s4 3 6 0 4-3 6 0" /></symbol>
  </defs>
</svg>

<esp-root icon-sprite-url="#brand-icons">
  <esp-button label="Say hi" icon="wave"></esp-button>
</esp-root>

Your symbol ids become the icon vocabulary for that root — the same naming rules apply (start with a letter or number; letters, numbers, dashes, underscores). A custom external sprite needs no new mechanism: icon-sprite-url="/my/sprite.svg" has always worked.

Light-DOM markup you write yourself needs no adoption: a plain <use href="#wave"> in page markup resolves against the document, where the inline sprite already lives.

Provenance

The shared sprite and inline SVG template modules are tracked in license/asset-provenance.json. Tabler-derived symbols are covered by license/THIRD_PARTY_NOTICES.md; Taproot-owned symbols, such as the logo, are identified separately in the provenance manifest.

When adding, replacing, or removing SVG assets, update the provenance manifest in the same change. If the new asset comes from a third party, add or update the required notice before publishing.

Components API Guides Getting started Styling Espalier Browser support GitHub npm package Taproot I/O