Frameworks

Icons in Next.js

glyphfox works in both routers. In the App Router the React components need a one file client boundary, and Server Components can print the same icons as plain SVG that ships no JavaScript at all.

Install

Install the package. It ships compiled ESM and CommonJS with type declarations, so there is nothing to add to next.config.js: no transpilePackages, no loader and no stylesheet.

terminal
npm install glyphfox

The React components come from glyphfox/react and are covered in depth in the React guide. This page is about what changes in Next.js: where components may run, how to keep icons out of the client bundle, and what the server renders.

Why the App Router needs a client boundary

In the App Router every module is a Server Component unless a file marks itself with "use client". The glyphfox components keep hover, click and morph state with useState, useEffect and useRef, and the package does not carry a "use client" directive. Rendering <HomeIcon /> straight from app/page.tsx therefore fails with an error about hooks that only work in Client Components.

The fix is not to turn whole pages into Client Components. Mark one small file as the boundary and import your icons through it. Server Components can render Client Components; the icons are still rendered to HTML on the server and then hydrated in the browser.

One client file for your icons

components/icons.ts
'use client';

export { HomeIcon, SearchIcon, BellIcon, MenuIcon, Icon, MorphIcon, Fusion } from 'glyphfox/react';
app/page.tsx
import { HomeIcon, SearchIcon, Fusion } from '@/components/icons';

export default function Page() {
  return (
    <header className="bar">
      <a href="/"><HomeIcon size={20} /> Home</a>
      <SearchIcon title="Search" />
      <Fusion icons={['cloud', 'lock']} layout="overlap" />
    </header>
  );
}

Re-export names one by one. Next.js does not support export * in a client boundary, and named exports keep tree shaking intact: only the icons listed in the file, and only the ones a page actually uses, end up in the client chunks.

Inside a file that already starts with "use client" you can import from glyphfox/react directly. The boundary file only exists for icons that you render from server files.

Interactive icons in Client Components

Event handlers and refs cannot cross from a Server Component into a Client Component, because props on that edge must be serializable. Anything that reacts to the user, such as a toggle with a morph, belongs in its own Client Component:

components/theme-toggle.tsx
'use client';

import { useState } from 'react';
import { MorphIcon } from 'glyphfox/react';

export function ThemeToggle() {
  const [dark, setDark] = useState(true);
  return (
    <button
      type="button"
      onClick={() => setDark((d) => !d)}
      aria-label={dark ? 'Switch to the light theme' : 'Switch to the dark theme'}
    >
      <MorphIcon name={dark ? 'moon' : 'sun'} duration={300} />
    </button>
  );
}

The server renders the starting icon, and the morph plays in the browser once the component has hydrated. Keep the first state identical on both sides: reading localStorage or the time during render makes the server and the client disagree and React reports a hydration mismatch.

Zero JavaScript icons in Server Components

Most icons never change after the page loads. For those, a Server Component can turn icon data into an SVG string with renderToString from glyphfox/core. The code runs only on the server, so the icons arrive as markup and add nothing to the client bundle.

components/server-icon.tsx
import { renderToString } from 'glyphfox/core';
import type { IconData, SvgOptions } from 'glyphfox';

type Props = SvgOptions & { icon: IconData };

export function ServerIcon({ icon, ...options }: Props) {
  return <span className="icon" dangerouslySetInnerHTML={{ __html: renderToString(icon, options) }} />;
}
app/docs/page.tsx
import fileText from 'glyphfox/icons/file-text';
import externalLink from 'glyphfox/icons/external-link';
import { ServerIcon } from '@/components/server-icon';

export default function Docs() {
  return (
    <a href="https://example.com/handbook">
      <ServerIcon icon={fileText} size={18} /> Handbook
      <ServerIcon icon={externalLink} size={14} title="opens in a new tab" />
    </a>
  );
}

Each glyphfox/icons/NAME import is one small data module. When you want names and the full name syntax (file+check, cloud&lock:side, heart/solid), use toSvg from the main entry instead. It brings the whole icon registry, but in a Server Component that only weighs on the server:

app/status/page.tsx
import { toSvg } from 'glyphfox';

export default function Status() {
  return <span className="icon" dangerouslySetInnerHTML={{ __html: toSvg('file+check', { size: 20 }) }} />;
}

Options use the core names: class instead of className, and style as a string. A wrapper element is needed for dangerouslySetInnerHTML; make it behave like the icon itself so it adds no line height:

app/globals.css
.icon {
  display: inline-flex;
  line-height: 0;
  vertical-align: middle;
}

Living states still work without JavaScript, because the animation is a small stylesheet inside the SVG: renderToString(loader, { animate: 'spin' }) spins, and renderToString(menu, { morphTo: x, trigger: 'hover' }) morphs on hover with a CSS transition. Click and state triggers need a Client Component.

Icons by name on the server

Icon, morphTo and Fusion look names up in a registry that each imported icon component fills. A name nobody imported is fetched with a dynamic import after the page loads, which means the server sends an empty SVG for it and the icon pops in after hydration. Register the data of every name you render from strings in the boundary file; it runs on the server during rendering and again in the browser:

components/icons.ts
'use client';

import { registerIcons } from 'glyphfox/react';
import { iconSettings, iconUsers, iconChartBar } from 'glyphfox';

registerIcons([iconSettings, iconUsers, iconChartBar]);

export { Icon, HomeIcon } from 'glyphfox/react';

The data exports are tree shaken like the components. Watch out for a difference between environments: development builds may evaluate every icon module, so a missing registration can look fine with next dev and render empty only after next build, where unused modules are dropped.

Pages Router

The Pages Router has no Server Components, so the components work as is in pages, layouts and _app. Every page is rendered on the server and hydrated, which makes the registration above just as useful: put registerIcons in pages/_app.tsx if you render icons by name.

pages/_app.tsx
import type { AppProps } from 'next/app';
import { registerIcons } from 'glyphfox/react';
import { iconSettings, iconUsers } from 'glyphfox';

registerIcons([iconSettings, iconUsers]);

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

An icon as the favicon

The App Router picks up app/icon.svg as the site icon. The CLI writes any icon there with an explicit color, since a favicon has no surrounding text color for currentColor to follow:

terminal
npx glyphfox copy fox --color "#ff7a1a" --weight 2.5 --out app/icon.svg

For PNG app icons in several sizes, the brand kit in the studio exports a favicon set themed with your color, weight and style.

What the build output contains

With webpack, next build emits many tiny client chunks, roughly one per icon in the set. They come from the on demand loader behind Icon and morphTo: its dynamic import points at the icon folder, so the bundler prepares a chunk for every icon that could be requested by name. They are a few hundred bytes each and are only downloaded when a page renders a name that was not imported. Whatever the bundler, registering the names you render from strings (above) means they are never requested. The icons you import directly live in your regular chunks. Measurements are in the tree shaking guide.

Props

The components take the same props as in any React app. When a Server Component renders them through the boundary, every prop below is a plain value and can cross it; only callbacks such as onClick and ref must be set inside a Client Component. The core options of renderToString and toSvg use the same names.

PropTypeDefaultDescription
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

Hook errors in Server Components
Importing from glyphfox/react in a server file fails because the components use hooks. Import through a "use client" file, or render static SVG with renderToString.
export * in the boundary
Next.js rejects export * in a client boundary. List the icons with named exports.
Functions as props from the server
onClick, onMouseEnter and refs are not serializable. Move interactive icons into a Client Component and pass only data down.
Empty icons after next build
An Icon name that was never imported renders empty on the server. Register its data with registerIcons in the boundary file or in _app.
Unknown names throw
toSvg throws on a name that does not exist, which turns into a server error. Check user supplied names against iconNames from glyphfox before rendering them.
Extra space around server icons
The wrapper of dangerouslySetInnerHTML is an inline element with its own line box. Give it display: inline-flex and line-height: 0.

Questions

Do I need transpilePackages for glyphfox?

No. The package ships plain JavaScript in ESM and CommonJS with type declarations, so Next.js resolves and bundles it like any compiled dependency.

Can I use the glyph-fox web component in Next.js?

Yes, import glyphfox/web from a Client Component. The module is safe to evaluate during server rendering, but a custom element only draws once the browser upgrades it, so the server HTML has an empty element where the icon will be. In a React app the components are the better fit because their SVG is in the server HTML; the web components guide covers the element.

Icons in this guide