Skip to content
Astro as patterns 4 min read

Icons with astro-icon

Icons are easy to get wrong: web fonts block rendering, inline <img> tags lose CSS control, and pulling a runtime icon library into a React island ships kilobytes of JavaScript you do not need. The astro-icon integration solves this by rendering icons as plain inline SVG at build time, drawing from Iconify’s catalog of over 200,000 open-source icons across 150+ icon sets. Because the SVG is emitted during the build, the result is pure markup — no client JavaScript, no font requests, and full control over color and size through CSS. This page covers installing the integration, rendering icons inline and as a shared sprite, styling them, and using your own custom SVGs.

Installing the integration

Add astro-icon with the Astro CLI, which wires the integration into your config automatically. You then install the Iconify icon sets you actually want as dev dependencies, since each set ships as its own npm package of raw SVG data.

npx astro add astro-icon
npm install @iconify-json/mdi @iconify-json/lucide

The CLI registers the integration in astro.config.mjs. The final config looks like this:

// astro.config.mjs
import { defineConfig } from "astro/config";
import icon from "astro-icon";

export default defineConfig({
  integrations: [icon()],
});

Icon set packages live under the @iconify-json/* scope, one package per set. Installing only the sets you use keeps your node_modules lean — the integration reads SVG data straight from these packages at build time and never bundles the whole catalog.

Rendering an icon inline

Import the Icon component and reference an icon by its pack:name identifier. The name prop maps to the Iconify package: mdi:home resolves to the home icon in the @iconify-json/mdi set. At build time the component is replaced with an inline <svg> element, so nothing ships to the browser except the markup itself.

---
// src/components/Header.astro
import { Icon } from "astro-icon/components";
---
<nav class="toolbar">
  <a href="/"><Icon name="mdi:home" /></a>
  <a href="/search"><Icon name="lucide:search" /></a>
  <a href="/settings"><Icon name="mdi:cog" /></a>
</nav>

The rendered output is a clean, accessible SVG that inherits the surrounding text color via currentColor:

Output:

<svg width="1em" height="1em" viewBox="0 0 24 24" ...>
  <path fill="currentColor" d="M10 20v-6h4v6h5v-8h3..." />
</svg>

Styling, sizing, and accessibility

Because each icon is real SVG inheriting currentColor, you size and color it with ordinary CSS. The size prop sets both width and height in pixels, while width and height let you override them independently. Pass any HTML attribute — class, style, aria-* — straight through to the underlying element.

---
import { Icon } from "astro-icon/components";
---
<Icon name="mdi:heart" size={32} class="text-rose-500" />
<Icon name="lucide:loader" width={20} height={20} />
<Icon name="mdi:alert" style="color: orange;" />

Icons are decorative by default and receive aria-hidden="true" automatically. When an icon conveys meaning on its own — such as a standalone icon button — provide a title prop, which astro-icon turns into an accessible <title> element and the matching ARIA role.

<button>
  <Icon name="mdi:trash-can" title="Delete item" />
</button>
PropTypePurpose
namestringpack:icon identifier (or local SVG name)
sizenumberSets width and height together, in px
width / heightnumberOverride a single dimension
titlestringAccessible label; adds <title> + role
class / stylestringPassed through to the <svg> element

Inline SVG versus a shared sprite

By default every <Icon> emits its full SVG inline. That is perfect for a handful of distinct icons, but if the same icon repeats dozens of times across a page — a star rating, a list bullet — duplicating the paths bloats the HTML. The SpriteSheet and Sprite components solve this by emitting each icon’s geometry once in a hidden <symbol> and referencing it with <use> elsewhere.

---
// src/layouts/BaseLayout.astro
import { Sprite } from "astro-icon/components";
---
<body>
  <Sprite.Provider>
    <slot />
  </Sprite.Provider>
</body>

Inside the provider, render icons with Sprite instead of Icon. The first occurrence defines the symbol; every subsequent use is a lightweight reference.

---
import { Sprite } from "astro-icon/components";
---
{Array.from({ length: 5 }).map(() => <Sprite name="mdi:star" />)}

Use inline Icon for one-off icons and the Sprite family when one icon repeats many times on the same page. Sprites trade a tiny upfront symbol definition for far smaller markup at scale.

Custom local icons

You are not limited to Iconify. Drop your own .svg files into src/icons/ and reference them by filename with no pack prefix. This is the standard place for brand logos or bespoke artwork that no public set provides.

src/
  icons/
    logo.svg
    badge.svg
---
import { Icon } from "astro-icon/components";
---
<Icon name="logo" />
<Icon name="badge" size={48} />

To use a different directory, point the integration at it through the iconDir option in your config so the loader knows where to find local SVGs.

// astro.config.mjs
import icon from "astro-icon";

export default defineConfig({
  integrations: [icon({ iconDir: "src/assets/icons" })],
});

Best Practices

  • Install only the @iconify-json/* sets you actually use rather than pulling in many sets “just in case.”
  • Let icons inherit currentColor and size them with CSS so they adapt to surrounding text and theme changes for free.
  • Reach for the Sprite components only when a single icon repeats many times on a page; otherwise inline Icon keeps things simplest.
  • Always pass a title to standalone, meaning-bearing icons (like icon-only buttons) and leave decorative icons aria-hidden.
  • Keep custom brand SVGs in src/icons/ so they share the same <Icon> API as Iconify icons.
  • Prefer astro-icon over runtime icon libraries inside islands — build-time SVG ships zero JavaScript and never blocks hydration.
Last updated June 14, 2026
Was this helpful?