SVG Icons in Astro: The Complete Guide (Zero-JS, Iconify & lucide-react)

How to use SVG icons in Astro — static inline SVGs, astro-icon, @iconify/svelte in islands, lucide-react in React islands, and zero-JS patterns for maximum performance.

Amit Yadav
Amit Yadav

Astro’s island architecture changes how SVG icons should be used. Most icons in a static Astro page should ship with zero JavaScript — they’re layout, not behavior. Only icons inside interactive islands (React, Vue, Svelte components) need client-side code. Getting this right cuts your JavaScript bundle significantly.

This guide covers every approach from fully static to fully interactive, with performance tradeoffs at each level.

Astro’s icon performance model

ApproachJS shippedSSR-safeRecommended for
Inline SVG in .astro0 bytesYesStatic icons everywhere
astro-icon package0 bytesYesLarge icon sets, clean DX
lucide-react in React islandOnly for that islandYesInteractive icon-heavy components
@iconify/vue in Vue islandOnly for that islandYesVue islands
Iconify CDN API (img tag)0 bytesYesOne-off preview icons
Default to zero JS for icons

If an icon is decorative or informational (not part of an interactive component), use astro-icon or inline SVG. Shipping a React island just to render a static bell icon is a performance anti-pattern.

Option 1: Inline SVG (zero JS, zero dependencies)

The simplest approach for custom or one-off icons — paste the SVG directly into your .astro template.

---
// No imports needed
---

<button class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="18"
    height="18"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    aria-hidden="true"
  >
    <circle cx="11" cy="11" r="8"/>
    <path d="m21 21-4.35-4.35"/>
  </svg>
  Search
</button>

For reusable inline icons, create a small .astro component:

---
// src/components/icons/SearchIcon.astro
const { size = 20, class: className = '' } = Astro.props;
---

<svg
  xmlns="http://www.w3.org/2000/svg"
  width={size}
  height={size}
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
  class={className}
  aria-hidden="true"
>
  <circle cx="11" cy="11" r="8"/>
  <path d="m21 21-4.35-4.35"/>
</svg>
---
import SearchIcon from '@/components/icons/SearchIcon.astro';
---

<SearchIcon size={18} class="text-gray-500" />

astro-icon is the standard icon solution for Astro. It integrates directly with Iconify, serving all 250,000+ icons with zero runtime JavaScript — icons are inlined as SVG during the build.

npm install astro-icon @iconify-json/lucide
# Install individual packs you need:
npm install @iconify-json/tabler @iconify-json/ph
// astro.config.mjs
import { defineConfig } from 'astro/config';
import icon from 'astro-icon';

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

Usage

---
import { Icon } from 'astro-icon/components';
---

<!-- Format: pack:icon-name -->
<Icon name="lucide:search" class="w-5 h-5" />
<Icon name="tabler:dashboard" class="w-5 h-5" />
<Icon name="ph:heart-duotone" class="w-5 h-5 text-blue-500" />

Icons are resolved at build time and inlined as SVG — no CDN fetch, no JS bundle, no runtime cost.

Size and color control

<Icon name="lucide:settings" size={20} class="text-gray-600 dark:text-gray-400" />

The size prop sets width and height. Color inherits from currentColor via text-* Tailwind classes.

Install only the packs you use

@iconify/json (~200MB) installs all icon packs. Install individual packs (@iconify-json/lucide) to keep node_modules lean. Astro only bundles icons you actually reference.

Option 3: lucide-react in React islands

For icon-heavy interactive components (dropdowns, comboboxes, animated panels), put the icon library inside the React island and let Astro handle the rest statically.

---
// A static Astro page — no JS for the surrounding layout
import CommandPalette from '@/components/CommandPalette';
---

<html>
  <body>
    <!-- Static nav icons — zero JS -->
    <Icon name="lucide:home" class="w-5 h-5" />

    <!-- Interactive island — lucide-react ships only for this -->
    <CommandPalette client:load />
  </body>
</html>
// src/components/CommandPalette.tsx (React island)
'use client'; // Not needed in Astro — just a React component
import { Search, Command, ArrowRight } from 'lucide-react';

export function CommandPalette() {
  // ...interactive logic here
  return (
    <div>
      <Search size={16} />
      {/* ... */}
    </div>
  );
}

Hydration directives

Astro’s hydration directives control when the icon-containing island becomes interactive:

DirectiveWhen JS loadsUse for
client:loadImmediatelyAbove-the-fold interactive icons
client:idleBrowser is idleBelow-fold interactive elements
client:visibleWhen scrolled into viewLazy icon grids
client:onlyAlways JS, no SSRIcons that depend on browser APIs
<!-- Icon-heavy sidebar — loads when visible -->
<AnimatedSidebar client:visible />

<!-- Static icon in layout — no hydration -->
<Icon name="lucide:menu" class="w-5 h-5" />

Dark mode in Astro with Tailwind

astro-icon and inline SVGs both use currentColor, so Tailwind’s dark: variant applies without any extra setup:

<div class="text-gray-700 dark:text-gray-300">
  <Icon name="lucide:settings" size={20} />
</div>

For script-driven dark mode toggle (localStorage persisted), put the toggle script in <head> to prevent flash:

<head>
  <script is:inline>
    const theme = localStorage.getItem('theme') ?? 'light';
    document.documentElement.classList.toggle('dark', theme === 'dark');
  </script>
</head>

Accessibility

<!-- Decorative -->
<Icon name="lucide:arrow-right" class="w-4 h-4" aria-hidden="true" />

<!-- Meaningful standalone -->
<Icon
  name="lucide:alert-triangle"
  class="w-5 h-5 text-yellow-500"
  role="img"
  aria-label="Warning"
/>

<!-- In an icon-only link -->
<a href="/settings" aria-label="Settings">
  <Icon name="lucide:settings" class="w-5 h-5" aria-hidden="true" />
</a>

Performance checklist for Astro icons

  • Static layout icons use astro-icon or inline SVG (zero JS)
  • Only icons inside interactive islands use React/Vue icon libraries
  • @iconify-json/<pack> packages are installed individually, not @iconify/json
  • client:visible is used for icon-heavy components below the fold
  • Dark mode is handled via Tailwind text-* classes, not JS color injection

Frequently asked questions

Can I use Lucide in .astro files without a React island? Not directly — Lucide React exports JSX components. Use astro-icon with @iconify-json/lucide instead; it uses the exact same Lucide icon data, zero JS, zero React overhead.

Does astro-icon work in SSR mode (Node adapter)? Yes. Icons are resolved at request time and inlined in the HTML response. There’s no client-side fetch.

Can I build a custom icon with astro-icon? Yes — place custom SVG files in src/icons/ and reference them as <Icon name="local:my-icon" />.

Share this post