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.
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
'use client'; export { HomeIcon, SearchIcon, BellIcon, MenuIcon, Icon, MorphIcon, Fusion } from 'glyphfox/react';
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:
'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.
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) }} />; }
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:
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:
.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:
'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.
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:
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.
| Prop | Type | Default | Description |
|---|---|---|---|
size | number, string | 24 | Width and height; numbers are pixels. |
color | string | currentColor | Main color. |
secondaryColor | string | color | Color of the duotone layer and of the solid fill. |
secondaryOpacity | number | 0.3 / 1 | Opacity of the duotone layer or of the solid fill. |
weight | number | 2 | Stroke from 1 to 3 on the 24 grid, optically compensated below 24 px. |
absoluteWeight | boolean | false | Keeps the stroke at weight pixels at any size. |
corners | round, sharp | round | Round or sharp joins and caps. |
variant | outline, duotone, solid | outline | Style axis; solid comes from the closed shapes. |
rotate | number | 0 | Rotation in degrees. |
flipX | boolean | false | Mirror horizontally. |
flipY | boolean | false | Mirror vertically. |
mirrorRtl | boolean | false | Mirror in right to left layouts. |
animate | draw, pop, wiggle, spin, pulse, bounce, shake | none | Living state preset. |
trigger | mount, hover, click, state | mount | When the animation or the morph plays. |
active | boolean | false | The state for trigger state. |
duration | number (ms) | per preset | Animation or morph duration. |
delay | number (ms) | 0 | Animation or morph delay. |
loop | boolean, number | false | Repeat forever or a number of times (spin and pulse loop by default). |
modifier | plus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dot | none | Badge with an automatic knockout gap. |
modifierCorner | top-left, top-right, bottom-left, bottom-right | bottom-right | Corner of the badge. |
morphTo | name, data | none | Morph target; the trigger decides when it shows. |
title | string | none | Accessible name: title, role img and aria-label. |
decorative | boolean | true without a title | Hides the icon from assistive technology. |
Pitfalls
- Hook errors in Server Components
- Importing from
glyphfox/reactin a server file fails because the components use hooks. Import through a"use client"file, or render static SVG withrenderToString. 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,onMouseEnterand refs are not serializable. Move interactive icons into a Client Component and pass only data down.- Empty icons after
next build - An
Iconname that was never imported renders empty on the server. Register its data withregisterIconsin the boundary file or in_app. - Unknown names throw
toSvgthrows on a name that does not exist, which turns into a server error. Check user supplied names againsticonNamesfromglyphfoxbefore rendering them.- Extra space around server icons
- The wrapper of
dangerouslySetInnerHTMLis an inline element with its own line box. Give itdisplay: inline-flexandline-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.