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 yournode_moduleslean — 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>
| Prop | Type | Purpose |
|---|---|---|
name | string | pack:icon identifier (or local SVG name) |
size | number | Sets width and height together, in px |
width / height | number | Override a single dimension |
title | string | Accessible label; adds <title> + role |
class / style | string | Passed 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
Iconfor one-off icons and theSpritefamily 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
currentColorand size them with CSS so they adapt to surrounding text and theme changes for free. - Reach for the
Spritecomponents only when a single icon repeats many times on a page; otherwise inlineIconkeeps things simplest. - Always pass a
titleto standalone, meaning-bearing icons (like icon-only buttons) and leave decorative iconsaria-hidden. - Keep custom brand SVGs in
src/icons/so they share the same<Icon>API as Iconify icons. - Prefer
astro-iconover runtime icon libraries inside islands — build-time SVG ships zero JavaScript and never blocks hydration.