Frameworks

Icons in Preact

Preact runs the glyphfox React components through preact/compat without code changes. If your app does not load compat, the glyph-fox web component works in plain Preact.

Install

Install glyphfox next to Preact. preact/compat is part of the preact package, so there is no second dependency to add.

terminal
npm install glyphfox

You have two ways to render icons, and they share the same rendering engine and the same props:

  • The components from glyphfox/react, run by Preact through compat. Best when your app already aliases React or uses other React packages.
  • The web component <glyph-fox>, which needs no compat at all. Best when you keep Preact lean and only need icons.

How the components run on Preact

The React entry point imports exactly six things from react: createElement, forwardRef, useState, useEffect, useMemo and useRef. compat implements all of them, so once the bundler resolves react to preact/compat the components render with Preact's own renderer, in the same tree and with the same hooks as the rest of your app.

The SVG attributes are created with React names such as strokeWidth and strokeLinecap, and compat turns them back into stroke-width and stroke-linecap on the element. The living state animations are a <style> element inside the SVG, set through dangerouslySetInnerHTML, which Preact supports as well.

Vite

The official Preact preset for Vite already aliases react, react-dom and the JSX runtime to compat. Nothing else is needed:

vite.config.js
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
  plugins: [preact()],
});

webpack and esbuild

With webpack, add the aliases to resolve.alias. glyphfox only needs react, but aliasing the whole set keeps any other React package in your app on the same Preact instance:

webpack.config.js
module.exports = {
  resolve: {
    alias: {
      react: 'preact/compat',
      'react-dom/test-utils': 'preact/test-utils',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
};

esbuild has the same option. Turn on code splitting at the same time, otherwise esbuild inlines every icon module that the on demand loader can reach (the tree shaking guide explains why):

build.mjs
import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/main.jsx'],
  bundle: true,
  splitting: true,
  format: 'esm',
  outdir: 'dist',
  jsx: 'automatic',
  jsxImportSource: 'preact',
  alias: { react: 'preact/compat', 'react-dom': 'preact/compat' },
});

TypeScript

The declarations of glyphfox/react are written against the React types (ForwardRefExoticComponent, SVGProps). Map react to compat in paths so they resolve to Preact's types. If the checker still reports problems inside the declaration files of dependencies, keep skipLibCheck on, as most Preact templates do:

tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "skipLibCheck": true,
    "paths": {
      "react": ["./node_modules/preact/compat/"],
      "react-dom": ["./node_modules/preact/compat/"]
    }
  }
}

Use the components

Import icons by name and use Preact hooks around them as usual. Everything from the React guide applies: Icon with the name syntax, Fusion, MorphIcon, useMorph, modifiers and refs.

Card.jsx
import { useState } from 'preact/hooks';
import { HeartIcon, MorphIcon, Fusion } from 'glyphfox/react';

export function Card() {
  const [liked, setLiked] = useState(false);
  return (
    <article className="card">
      <button type="button" onClick={() => setLiked(!liked)} aria-pressed={liked}>
        <HeartIcon animate="pop" trigger="state" active={liked} variant={liked ? 'solid' : 'outline'} />
        Like
      </button>
      <MorphIcon name={liked ? 'check' : 'plus'} duration={250} />
      <Fusion icons={['user', 'shield']} layout="badge" />
    </article>
  );
}

Refs go through the forwardRef of compat, so ref on an icon points at the rendered <svg> element, ready for getBoundingClientRect() or the Web Animations API. Event handlers such as onClick and attributes such as data-* pass straight through to the element, exactly as in React.

With signals, pass the value rather than the signal. Preact unwraps signals bound to DOM attributes, but an icon component receives the signal object itself and reads its props as plain values, so it would neither read the right value nor update when the signal changes:

Status.jsx
import { signal } from '@preact/signals';
import { CloudIcon } from 'glyphfox/react';

const syncing = signal(false);

export function Status() {
  return <CloudIcon modifier={syncing.value ? 'sync' : undefined} />;
}

Without compat: the web component

The <glyph-fox> custom element renders the same icons with the same props as attributes in kebab-case. Import glyphfox/web once to define it with every icon, or define it yourself and register only the icons you use:

icons.js
import { defineGlyphFox } from 'glyphfox/web/element';
import { registerIcons } from 'glyphfox/core';
import { iconHome, iconSearch, iconMenu, iconX } from 'glyphfox';

registerIcons([iconHome, iconSearch, iconMenu, iconX]);
defineGlyphFox();
Header.jsx
import { useState } from 'preact/hooks';
import './icons.js';

export function Header() {
  const [open, setOpen] = useState(false);
  return (
    <header>
      <glyph-fox name="home" size="20"></glyph-fox>
      <glyph-fox name="search" weight="1.5" title="Search"></glyph-fox>
      <button type="button" onClick={() => setOpen(!open)} aria-expanded={open} aria-label="Menu">
        <glyph-fox name={open ? 'x' : 'menu'} morph></glyph-fox>
      </button>
    </header>
  );
}

Preact sets unknown props on custom elements as attributes, which is what the element listens to, so dynamic values such as name={open ? 'x' : 'menu'} work and the morph attribute animates every change. In TypeScript, add glyph-fox to Preact's JSX.IntrinsicElements in a declaration file, the way the Preact documentation describes for any custom element.

Attributes, sizing inside shadow DOM and server rendering of the element are covered in the web components guide.

Which path is lighter

Both paths ship the same engine (rendering, axes, modifiers, fusion and morph), so the icons cost about the same either way. The real difference is compat: if your app already loads it for other packages, the components add nothing new; if it does not, the web component avoids it. In both cases import only the icons you use, as shown above.

Props

The components take these props. On <glyph-fox> the same props are attributes in kebab-case (secondary-color, flip-x, morph-to), plus name, layout, gap and morph.

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

A missing alias
Without the alias the bundler resolves react to the real React package (or fails to find it). The icons then call hooks from a different renderer, which shows up as invalid hook call errors or icons that never update. Check that react resolves to preact/compat.
class instead of className
The components merge className into their own glyphfox glyphfox-NAME classes. A class prop is not merged and can be replaced by them, so use className on icon components.
Two copies of Preact
compat must resolve to the same preact as your app. A duplicate copy in node_modules breaks hooks in the same way as a missing alias; deduplicate it in your package manager.
Signals as props
Pass signal.value to icon components. The signal object itself is not unwrapped for components.
Empty elements before definition
<glyph-fox> draws only after defineGlyphFox() has run in the browser. With prerendered HTML, give the element a size in CSS so the layout does not shift while the script loads.

Questions

Do I need preact/compat to use glyphfox?

Only for the components from glyphfox/react. The <glyph-fox> element and toSvg from glyphfox work in plain Preact without it.

Do the components render with preact-render-to-string?

Yes. Through compat they render on the server like any React component, and the SVG is in the HTML. Icons rendered by name with Icon need their data registered first, as described in the server rendering guide.

Icons in this guide