Frameworks

Icons in Astro

Astro renders components to HTML on the server or at build time, which suits SVG icons: call the renderer in the frontmatter and the page receives plain inline SVG with no client JavaScript. Islands and the custom element cover the icons that need to react.

Install

Install the package. Static icons need no Astro integration, because glyphfox is plain ES modules that run in the frontmatter. Islands built with glyphfox/react, glyphfox/vue or glyphfox/svelte need the matching Astro integration.

terminal
npm install glyphfox
# only for islands
npx astro add react

Icons in the frontmatter

Code in the frontmatter runs on the server, or at build time for static pages. toSvg returns the SVG string for any icon name, and set:html writes it into the page without escaping it:

src/pages/index.astro
---
import { toSvg } from 'glyphfox';

const search = toSvg('search', { size: 20 });
---
<label class="search">
  <Fragment set:html={search} />
  <input type="search" name="q" aria-label="Search the docs" />
</label>

toSvg takes the full name syntax, so badges, fusions and styles fit in one string: toSvg('file+check@tr'), toSvg('cloud&lock:overlap'), toSvg('heart/solid').

Importing glyphfox loads every icon, but only on the server or in the build. None of it reaches the browser, because the frontmatter never runs there.

An Icon.astro component

Wrap the call in a component so pages pass props instead of options. The SVG goes straight into the parent, without a wrapper element:

src/components/Icon.astro
---
import { toSvg, type SvgOptions } from 'glyphfox';

interface Props extends SvgOptions {
  name: string;
}

const { name, ...options } = Astro.props;
const svg = toSvg(name, options);
---
<Fragment set:html={svg} />
src/components/Nav.astro
---
import Icon from './Icon.astro';
---
<nav class="nav">
  <a href="/"><Icon name="home" size={20} /> Home</a>
  <a href="/docs/"><Icon name="book-open" size={20} weight={1.5} /> Docs</a>
  <a href="/new/"><Icon name="file+plus" size={20} title="New file" /></a>
</nav>

<style>
  .nav :global(.glyphfox) { color: #6b7280; }
  .nav a:hover :global(.glyphfox) { color: #111827; }
</style>

Every svg carries the classes glyphfox and glyphfox-NAME. Markup inserted with set:html does not receive the attributes Astro uses to scope component styles, so select it with :global() inside a scoped parent selector, as above, or let it inherit color through currentColor.

The icon link above has no text, so its icon gets a title: that adds role="img", aria-label and a <title>, and the link takes its name from it. Icons next to visible text stay decorative. More patterns are in the accessibility guide.

Motion without JavaScript

The SVG can carry its own CSS, so living states and hover morphs play in static HTML with no script: animate with the default trigger="mount" or with "hover", and morphTo with trigger="hover", which reacts to the svg and to its direct parent.

src/components/Chapter.astro
---
import Icon from './Icon.astro';
---
<a class="next" href="/chapter-2/">
  Next chapter <Icon name="arrow-right" morphTo="chevron-right" trigger="hover" />
</a>

<p class="status">
  <Icon name="loader" animate="spin" title="Building" /> Building the preview
</p>

Because Icon.astro adds no wrapper, the svg is a direct child of the link, and hovering anywhere on the link morphs the arrow. Every preset stops under prefers-reduced-motion; the presets and triggers are listed in animated icons.

Islands for state

Icons that follow state, such as a menu button that morphs into a close icon, belong in an island. Use the framework package of the island and hydrate it with a client directive:

src/components/MenuButton.jsx
import { useState } from 'react';
import { MorphIcon, registerIcons } from 'glyphfox/react';
import menu from 'glyphfox/icons/menu';
import x from 'glyphfox/icons/x';

// Names passed to MorphIcon load on demand; registering them
// puts the icon in the server rendered HTML.
registerIcons([menu, x]);

export default function MenuButton() {
  const [open, setOpen] = useState(false);
  return (
    <button
      type="button"
      aria-expanded={open}
      aria-label={open ? 'Close menu' : 'Open menu'}
      onClick={() => setOpen(!open)}
    >
      <MorphIcon name={open ? 'x' : 'menu'} duration={300} />
    </button>
  );
}
src/layouts/Base.astro
---
import MenuButton from '../components/MenuButton.jsx';
---
<header>
  <MenuButton client:load />
</header>

Astro renders islands on the server first. Icon components such as MenuIcon register their icon when your code uses them, but names passed to MorphIcon, Icon or morphTo are loaded on demand, so without registerIcons the server HTML holds an empty svg until the island hydrates.

The same works with glyphfox/vue and with glyphfox/svelte, which needs Svelte 5. For islands below the fold, client:visible or client:idle delays the download until it is needed. The framework details are in the React, Vue and Svelte guides.

The custom element

Without a framework, the <glyph-fox> element covers clicks, hover and name changes. Astro processes and bundles the <script> tags of components, and includes each script once per page, even when the component appears many times:

src/components/LikeButton.astro
<button type="button" class="like" aria-pressed="false" aria-label="Like">
  <glyph-fox name="heart" morph-to="heart/solid" trigger="state"></glyph-fox>
</button>

<script>
  import { defineGlyphFox } from 'glyphfox/web/element';
  import { registerIcons } from 'glyphfox/core';
  import heart from 'glyphfox/icons/heart';

  registerIcons([heart]);
  defineGlyphFox();

  for (const button of document.querySelectorAll('button.like')) {
    const icon = button.querySelector('glyph-fox');
    button.addEventListener('click', () => {
      const on = button.getAttribute('aria-pressed') !== 'true';
      button.setAttribute('aria-pressed', String(on));
      icon.toggleAttribute('active', on);
    });
  }
</script>

The button drives the icon through trigger="state" and the active attribute. With trigger="click" the element would listen on itself, and a click on the padding of the button would toggle aria-pressed without morphing the icon.

Custom elements stay defined across client side navigation with the Astro view transitions router, so icons on new pages upgrade on their own. Code that attaches listeners, like the loop above, runs once; with the router, run it again on the astro:page-load event.

Tailwind classes

With Tailwind CSS in the project, the glyphfox plugin draws icons as CSS masks, another option with no JavaScript, handy in content heavy templates. Setup and class syntax are in the Tailwind guide.

src/components/Hero.astro
<h1 class="flex items-center gap-3">
  <span class="gf-rocket text-4xl text-orange-500" aria-hidden="true"></span>
  Launch faster
</h1>

Props

Icon.astro passes its props to toSvg, so it takes the shared options with their camelCase names, plus class and style as strings for the svg. Unlike in the core renderer, morphTo can be a name here, because toSvg resolves it.

OptionTypeDefaultDescription
sizenumber, string24Width and height; numbers are pixels.
colorstringcurrentColorMain color.
secondaryColorstringcolorColor of the duotone layer and of the solid fill.
secondaryOpacitynumber0.3 / 1Opacity of the duotone layer or of the solid fill.
weightnumber2Stroke from 1 to 3 on the 24 grid, optically compensated below 24 px.
absoluteWeightbooleanfalseKeeps the stroke at weight pixels at any size.
cornersround, sharproundRound or sharp joins and caps.
variantoutline, duotone, solidoutlineStyle axis; solid comes from the closed shapes.
rotatenumber0Rotation in degrees.
flipXbooleanfalseMirror horizontally.
flipYbooleanfalseMirror vertically.
mirrorRtlbooleanfalseMirror in right to left layouts.
animatedraw, pop, wiggle, spin, pulse, bounce, shakenoneLiving state preset.
triggermount, hover, click, statemountWhen the animation or the morph plays.
activebooleanfalseThe state for trigger state.
durationnumber (ms)per presetAnimation or morph duration.
delaynumber (ms)0Animation or morph delay.
loopboolean, numberfalseRepeat forever or a number of times (spin and pulse loop by default).
modifierplus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dotnoneBadge with an automatic knockout gap.
modifierCornertop-left, top-right, bottom-left, bottom-rightbottom-rightCorner of the badge.
morphToname, datanoneMorph target; the trigger decides when it shows.
titlestringnoneAccessible name: title, role img and aria-label.
decorativebooleantrue without a titleHides the icon from assistive technology.

Pitfalls

Scoped styles and set:html
Rules in a component style block do not match markup inserted with set:html, because it lacks the scoping attributes. Use :global(.glyphfox) inside a scoped selector, or inherit color.
Names in islands
MorphIcon, Icon and morphTo load names on demand. Register their data with registerIcons so the server HTML is complete.
Click triggers inside buttons
trigger="click" on glyph-fox listens on the icon, and clicks on the padding of the button do not reach it. Use trigger="state" and set active from the button.
Unknown names stop the build
toSvg throws for a name that does not exist, which fails a static build and returns an error in server mode. Check names from content against the icon list, or catch the error where they come from user data.
Server bundles on edge runtimes
toSvg from the main entry puts every icon in the server bundle, about 154 KB minified. Where function size is limited, use renderToString from glyphfox/core with per icon imports, and pass icon data to morphTo.
Wrappers and hover morphs
A hover morph reacts to the svg and to its direct parent. Wrapping the icon in an extra element makes that element the parent, so hovering the link around it no longer starts the morph.

Questions

Do icons rendered in the frontmatter ship any JavaScript?

No. The frontmatter runs on the server or at build time, and the page receives plain SVG. Presets and hover morphs are CSS inside that SVG. Only islands and the custom element add client code.

Can I use icons in Markdown or MDX content?

In MDX, import Icon.astro and use it like in any component. Plain Markdown cannot import components, so render the icons in the layout around the content, or use the Tailwind classes in raw HTML where your Markdown allows it.

Icons in this guide