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.
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:
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:
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):
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:
{
"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.
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:
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:
import { defineGlyphFox } from 'glyphfox/web/element'; import { registerIcons } from 'glyphfox/core'; import { iconHome, iconSearch, iconMenu, iconX } from 'glyphfox'; registerIcons([iconHome, iconSearch, iconMenu, iconX]); defineGlyphFox();
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.
| 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
- A missing alias
- Without the alias the bundler resolves
reactto 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 thatreactresolves topreact/compat. classinstead ofclassName- The components merge
classNameinto their ownglyphfox glyphfox-NAMEclasses. Aclassprop is not merged and can be replaced by them, so useclassNameon icon components. - Two copies of Preact
- compat must resolve to the same
preactas your app. A duplicate copy innode_modulesbreaks hooks in the same way as a missing alias; deduplicate it in your package manager. - Signals as props
- Pass
signal.valueto icon components. The signal object itself is not unwrapped for components. - Empty elements before definition
<glyph-fox>draws only afterdefineGlyphFox()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.