IcoMoon Icons logo

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: