IcoMoon + React: three ways in, and which one to pick
The direct answer: in a React codebase, prefer inline SVG components over the icon font. Copy each glyph you need as its own component — every one of the 491 free icons on this site ships a ready React snippet on its icon page — or generate a single Icon component from selection.json at build time. The classic font-plus-className route still works, but in React it carries two traps — dynamically built class names and an SSR flash — that the other two patterns avoid entirely. All three, with real code and honest costs, below.
Pattern 1 — One inline SVG component per icon
This is the pattern React itself pushes you toward. Each glyph becomes a tiny component: fill="currentColor" makes it inherit text color, width="1em" makes it scale with font size, and because it is plain JSX there is no font request, no extra stylesheet and nothing to flash on first paint. Unused icons tree-shake away like any other dead export. You do not have to hand-trace paths either: open any icon detail page here — the home icon, say — and the export-ready React snippet is sitting under "Use it in code". Paste it, add a type, done:
// HomeIcon.tsx — pasted from this site's icon page, then typed
import type { SVGProps } from "react";
export const HomeIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M16 9.226l-8-6.21-8 6.21v-2.532l8-6.21 8 6.21zM14 9v6h-4v-4h-4v4h-4v-6l6-4.5z" />
</svg>
);
// Inherits color and size from the parent — no CSS file, no font:
// <button className="nav-link"><HomeIcon /> Home</button> The cost profile is hard to beat: a typical app uses 15–30 icons, and each component is a few hundred bytes after compression — compare that with shipping the full 49 KB WOFF2 plus a 22 KB stylesheet to render three glyphs. The only real downside is ceremony: one file (or one export) per icon, which gets tedious past a few dozen. That is exactly where pattern 3 takes over.
Pattern 2 — The icon font and className, plus its traps
The traditional route: import the exported style.css once in your entry file, then render <span className="icon-home" aria-hidden="true" /> wherever a glyph goes. It works, and for a legacy codebase that already ships the font it is a fine steady state — the ready-made files live on our download page and the full class-to-unicode table on the cheat sheet. But know what you are signing up for in React specifically.
Trap 1: dynamically built class names
Sooner or later someone writes `icon-${name}` to pick glyphs from props. Two things go wrong. A typo fails silently — you get an empty span, no error, no glyph, nothing in the console. And if your build runs PurgeCSS or a similar content scanner over the icon stylesheet, a composed class name never appears literally in your source, so the .icon-* rules get stripped from the production bundle: everything renders in dev, blank squares in prod. If you must map names to glyphs at runtime, keep a literal whitelist object instead of string concatenation.
Trap 2: the SSR flash
With Next.js or any server-rendered setup, the HTML paints before the font file arrives — and because icon glyphs live at Private Use Area code points like \e900, fallback fonts have nothing to substitute. Users see empty boxes or tofu on every cold load until the WOFF2 lands. font-display: block hides the flash but trades it for invisible icons during the blocking window. Inline SVG has no such window: the markup is the icon.
Pattern 3 — selection.json as a build input
Every zip the generator app exports includes selection.json — and that file contains the raw path data for every glyph you selected, on a 1024-unit grid. Instead of treating it as an opaque save file (though it is that too — the app tutorial covers re-importing it), treat it as source and feed it to one component:
// Icon.tsx — one component for every glyph in your export
import type { SVGProps } from "react";
import selection from "./selection.json";
const glyphs = new Map<string, string[]>(
selection.icons.map((i) => [i.properties.name, i.icon.paths]),
);
type Props = { name: string } & SVGProps<SVGSVGElement>;
export function Icon({ name, ...rest }: Props) {
const paths = glyphs.get(name);
if (!paths) return null; // or throw in dev builds
return (
<svg
viewBox="0 0 1024 1024" // selection.json glyphs sit on a 1024 grid
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
{...rest}
>
{paths.map((d, i) => (
<path key={i} d={d} />
))}
</svg>
);
} One import, every glyph, and when a designer re-exports with new icons you replace a single file and the whole app picks them up. The honest caveat: a direct JSON import ships path data for every selected glyph to the client, whether rendered or not. For a curated 20–40 icon selection that is negligible; for the entire 491-icon free set it is real weight. At that scale, move the lookup into a small codegen script that emits pattern-1 components at build time — you keep the single source of truth and regain tree-shaking.
Which pattern for which project?
| Pattern | What you pay | Pick it when |
|---|---|---|
| Inline SVG components | A file per icon; bytes only for what you render | New code, design systems, most apps |
| Icon font + className | 49 KB WOFF2 + stylesheet, purge and SSR traps | Legacy codebases already shipping the font |
| selection.json pipeline | All selected glyphs in the bundle, unless codegen | Teams that re-export often, larger icon sets |
Start with pattern 1 and graduate to pattern 3 when the per-file ceremony hurts. If SVG in React interests you beyond components, the sprite-based approach — external symbol files referenced with use — gets its own treatment in the SVG sprite guide, and the free set's sources are all public in the IcoMoon-Free repository if you want to script against them directly.
Both SVG patterns work unchanged with the premium sets, because the Ultimate pack includes the SVG sources for all 1,500+ icons — $59 one-time, no subscription. The full cost breakdown is on our pricing page, or go straight to the store: