Appearance
CDN Icons with SVG Sprites
When to use
Use this pattern when a prototype needs icons but you don't want to install an icon library or configure a bundler. This pattern fetches an SVG sprite sheet from a CDN at runtime, namespaces its IDs to avoid collisions, and injects it into the document. A small web component then renders any icon with a <use> reference.
The pattern
Two pieces work together:
- Sprite loader — fetches the sprite SVG, wraps it in a hidden container, namespaces every
idattribute, and appends it to<head>. - Icon component — a custom element (
<ui-icon>) that renders an SVG<use>element pointing at the namespaced symbol.
Using icons
Call loadIconSprite() once to fetch and inject the sprite, then drop <ui-icon> elements anywhere on the page. The name attribute selects a symbol from the sprite, and optional size, color, and stroke-width attributes control rendering:
html
<script type="module">
// Load the sprite once on page load
await loadIconSprite();
</script>
<!-- Then use icons anywhere -->
<ui-icon name="heart"></ui-icon>
<ui-icon name="star" size="32" color="gold"></ui-icon>
<ui-icon name="check" size="16" stroke-width="3"></ui-icon>Styling the component
The <ui-icon> element uses CSS custom properties set by its render method, so you can style it with a simple type selector and var() references:
css
ui-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--size, 24px);
height: var(--size, 24px);
}
ui-icon svg {
width: 100%;
height: 100%;
}Deferred loading
Defer the sprite fetch until after the page has loaded to reduce initial page weight:
js
document.addEventListener(
"DOMContentLoaded",
loadIconSprite
);Example: Multiple icon libraries
Namespace each library differently and reference them with the library attribute:
js
async function loadSprite(library, url) {
const response = await fetch(url);
const svgText = await response.text();
const container = document.createElement("div");
container.hidden = true;
container.innerHTML = svgText;
for (const el of container.querySelectorAll("[id]")) {
el.id = `${library}-${el.id}`;
}
document.head.appendChild(container);
}
await loadSprite(
"lucide",
"https://cdn.jsdelivr.net/npm/lucide-static@latest/sprite.svg"
);
await loadSprite(
"material",
"https://example.com/material-icons/sprite.svg"
);Reference icons from different libraries with the library attribute:
html
<ui-icon library="lucide" name="heart"></ui-icon>
<ui-icon library="material" name="favorite"></ui-icon>Sprite loader
The loader fetches the sprite SVG from a CDN, wraps it in a hidden <div>, and prefixes every id attribute with the library name so multiple sprite sheets can coexist without collisions:
js
async function loadIconSprite() {
const url =
"https://cdn.jsdelivr.net/npm/lucide-static@latest"
+ "/sprite.svg";
const response = await fetch(url);
const svgText = await response.text();
// Wrap in a hidden container
const container = document.createElement("div");
container.setAttribute("name", "icon-sprite");
container.hidden = true;
container.innerHTML = svgText;
// Namespace IDs to avoid collisions
for (const el of container.querySelectorAll("[id]")) {
el.id = `lucide-${el.id}`;
}
document.head.appendChild(container);
}The sprite contains <symbol> elements, each with an id. Namespacing prefixes every ID with the library name (lucide-) so multiple sprite sheets can coexist.
Icon component
The <ui-icon> custom element reads name, size, color, and stroke-width attributes, maps them to CSS custom properties, and renders an SVG <use> element pointing at the namespaced sprite symbol:
js
class UiIcon extends HTMLElement {
static defaults = {
library: "lucide",
size: "24",
color: "currentColor",
strokeWidth: "2",
};
static get observedAttributes() {
return ["name", "size", "color", "stroke-width"];
}
get iconHref() {
const library = this.getAttribute("library") ?? UiIcon.defaults.library;
const name = this.getAttribute("name");
return `#${library}-${name}`;
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
const d = UiIcon.defaults;
const size = this.getAttribute("size") ?? d.size;
const color = this.getAttribute("color") ?? d.color;
const sw = this.getAttribute("stroke-width") ?? d.strokeWidth;
this.style.setProperty("--size", size);
this.style.setProperty("--color", color);
this.style.setProperty("--stroke-width", sw);
this.innerHTML = `
<svg
width="var(--size)"
height="var(--size)"
stroke="var(--color)"
stroke-width="var(--stroke-width)"
fill="none"
>
<use href="${this.iconHref}"></use>
</svg>
`;
}
}
customElements.define("ui-icon", UiIcon);Trade-offs
Advantages:
- Zero npm install — works immediately in any HTML file
- Single HTTP request loads the entire icon set
- SVG
<use>references are lightweight and cacheable - CSS custom properties make theming straightforward
- Multiple libraries can coexist with namespacing
Disadvantages:
- Loads the full sprite even if only a few icons are used
- Requires runtime fetch — icons aren't available until the sprite loads
- ID namespacing is a manual convention, not enforced
- No tree shaking — all symbols stay in the sprite