Skip to content

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:

  1. Sprite loader — fetches the sprite SVG, wraps it in a hidden container, namespaces every id attribute, and appends it to <head>.
  2. 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