Topics

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 renderingServer outputJavaScript in the browser
React, Vue, Svelte and Angular componentscomplete svg markuponly for morphs and for click and state triggers
toSvg and renderToStringa complete svg stringnone
The glyph-fox elementan empty elementrequired: the icon appears when the element upgrades
The data-glyph runtimean empty <i>required: the runtime replaces it
Tailwind CSS classesan empty element styled by your CSSnone
SVG spritean svg with a use referencenone

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:

server output
<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.

icons.js
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.

server.js
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.

icon.js
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.

edge.js
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:

app.css
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.

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

Empty icons in the server HTML
An icon rendered by name that was never registered is an empty svg with the class glyphfox-empty on 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-fox has 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_modules is 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.

Icons in this guide