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:
- Go to tabler and click on an icon
- Click
Copy SVGto copy the raw SVG - Paste it into the defs section of the SVG file
- Rename the new
svgnode tosymbol - Delete the
xmlns,width,height, andclassattributes from the new symbol - Give the new symbol an
idattribute (maybe matching the tabler name) - 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.