Server rendering
glyphfox icons are plain SVG, so they render on the server like any other markup. The details that matter are where the icon data comes from, what happens at hydration, and which ways of rendering need JavaScript in the browser.
What each package renders on the server
Every package uses the same renderer, so an icon looks identical whether it was rendered by React on a server, by Vue in the browser or by toSvg in a build script. What differs is whether the server can produce the markup at all.
| Way of rendering | Server output | JavaScript in the browser |
|---|---|---|
| React, Vue, Svelte and Angular components | complete svg markup | only for morphs and for click and state triggers |
toSvg and renderToString | a complete svg string | none |
The glyph-fox element | an empty element | required: the icon appears when the element upgrades |
The data-glyph runtime | an empty <i> | required: the runtime replaces it |
| Tailwind CSS classes | an empty element styled by your CSS | none |
| SVG sprite | an svg with a use reference | none |
Framework specifics live in their own guides: the client boundary of the Next.js App Router, plugins in Nuxt, layouts in SvelteKit, zero JavaScript icons in Astro and Qwik, and SolidStart. This page covers what they have in common.
Icon data on the server
A component such as HomeIcon carries its own data and registers it when its module runs, so it always renders on the server. Everything that refers to an icon by name looks the name up in a registry instead: Icon name, morphTo, the parts of Fusion, MorphIcon, useMorph, morphStore and the name input of the Angular component.
A name that is not registered is loaded on demand with a dynamic import. That load only starts in the browser, after mount, so the server renders a placeholder of the right size and nothing else:
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="glyphfox glyphfox-empty" aria-hidden="true"></svg>
Register the names you use in a module that both the server entry and the client entry import. The data exports such as iconSettings are small and tree shaken like the components.
import { registerIcons } from 'glyphfox/react'; // or glyphfox/vue, glyphfox/svelte import { iconSettings, iconFile, iconCloud, iconLock, iconX } from 'glyphfox'; // Names used through Icon, morphTo, Fusion and MorphIcon. registerIcons([iconSettings, iconFile, iconCloud, iconLock, iconX]);
In Angular the same list goes to provideGlyphFox({ icons }), which runs on the server and in the browser; see the Angular guide.
When the server knows more than the client
The opposite problem is just as real. When the server runs the package without bundling it, as Node does for dependencies that stay external, importing anything from glyphfox/react or glyphfox/vue evaluates the whole entry point, and every one of the 419 icon modules registers itself. <Icon name="settings" /> then renders fully on the server.
The browser bundle is tree shaken, so settings is not registered there. The first client render produces the empty placeholder, the markup no longer matches the server HTML, and the framework reports a hydration mismatch before the icon loads again. Registering the names explicitly, as above, makes both sides agree.
SVG strings with toSvg
Outside a component framework, toSvg(spec, props) returns the markup as a string. It accepts the full name syntax and every shared prop, has no browser dependencies and does no I/O, so it works in server templates, static site generators, serverless functions and build scripts.
import express from 'express'; import { toSvg } from 'glyphfox'; const app = express(); app.get('/', (req, res) => { res.send(`<!doctype html> <title>Files</title> <ul> <li>${toSvg('file+check', { size: 18 })} report.pdf</li> <li>${toSvg('cloud&lock:overlap', { size: 18, title: 'Encrypted backup' })} backup.zip</li> </ul>`); }); app.listen(3000);
Rendering is fast, but modifiers, fusions and the solid style do real geometry work. For icons that repeat on every request, cache the strings; the output depends only on the spec and the props.
import { toSvg } from 'glyphfox'; const cache = new Map(); export function icon(spec, props = {}) { const key = spec + JSON.stringify(props); let svg = cache.get(key); if (svg === undefined) { svg = toSvg(spec, props); cache.set(key, svg); } return svg; }
toSvg from the main entry loads every icon, which is fine for a server process. Where code size matters, such as edge functions with a size limit, use renderToString from glyphfox/core with the data of each icon from glyphfox/icons/NAME; the tree shaking guide has the numbers.
import { renderToString } from 'glyphfox/core'; import download from 'glyphfox/icons/download'; export const downloadIcon = renderToString(download, { size: 18 });
Hydration, morphs and living states
The server renders the state an icon has at render time. Morphs are computed in the browser: MorphIcon, morphTo and the hooks start animating after hydration, because effects do not run on the server. An icon with morphTo, trigger="state" and active already true renders its target on the server, so a menu that starts open shows the x in the HTML.
Living states need no hydration at all. Their CSS is inside the SVG, so a mount animation plays as soon as the browser parses the HTML and a hover animation works before any script loads. A state animation that is already active renders with its gf-on class and plays from the HTML as well. Clicks and later changes of state need the framework, because it toggles the attribute and the class that start them. The prefers-reduced-motion rule travels in the same stylesheet and applies to server markup too.
The markup is deterministic. Animation classes, mask ids and morph classes are hashes of their content, not counters or random values, so the server and the browser produce the same attributes and ids never cause a hydration mismatch.
The web component on server rendered pages
The glyph-fox element draws into a shadow root when its script runs. It does not use declarative shadow DOM, so server HTML contains an empty element with no size, and the line around it moves when the icon appears. Reserve the space until the element is defined:
glyph-fox:not(:defined) { display: inline-block; width: 24px; height: 24px; vertical-align: middle; }
For icons above the fold, server render inline SVG with a component or toSvg and keep the element for parts of the page that change on the client. The web components guide covers the element in detail.
Emails
Server rendering often ends in an email template, and that is the one place where SVG strings do not help: many email clients do not display inline SVG or SVG images. Use PNG files there. The icon browser downloads any icon as a PNG at the size you need, with your color and weight.
toSvg options
toSvg and renderToString take the same props as the components, as plain options.
| Option | 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
- Empty icons in the server HTML
- An icon rendered by name that was never registered is an empty
svgwith the classglyphfox-emptyon the server. Register the names, or import the component for that icon. - Hydration mismatch warnings
- If the server renders a name the browser does not have yet, the markup differs on the first client render. The usual causes are an unbundled server import of a whole entry point and names that come from data. Register the names in shared code.
- Icons that depend on the browser
- An icon chosen from
localStorage, the color scheme or the window size cannot match on the server. Render a neutral icon on the server and switch after mount, or read the value from a cookie the server can see. - Layout shift from the element
- An undefined
glyph-foxhas no size. Reserve the space with:not(:defined)or render inline SVG on the server. - Morph targets in production builds
- In a Vite production build, names that are neither imported nor registered stay empty in the browser, because the on demand loader inside
node_modulesis not transformed. Server rendering does not hide this; registering the names fixes both sides.
Questions
Do the React components work in React Server Components?
Not directly: they use hooks, so they need a client boundary. In a Server Component you can render toSvg or renderToString output with dangerouslySetInnerHTML and ship no JavaScript for the icon. The Next.js guide shows both.
Why is my icon empty in the server HTML?
It is rendered by name, through Icon, morphTo, Fusion or MorphIcon, and its data was not registered when the server rendered. Call registerIcons with the data exports in a module both entries import.