React Icons: How to Use Icons in React

Install an icon package such as lucide-react or react-icons and import each icon by name, for example import { Camera } from "lucide-react". Named imports tree-shake, so only icons you use are bundled. Size icons with props or 1em, colour them through currentColor, and hide decorative ones with aria-hidden.

Quick picks
If you wantUse
One clean, consistent setlucide-react
Icons from many setsreact-icons
Two or three custom iconsAn inline SVG component
The same icons repeated a lotAn SVG sprite with <use>

Updated

This guide is about wiring icons into React code. For which free icon sets to choose and their licences, see our list of the best free icon libraries. The size numbers here come from our own builds, with the tool and settings stated.

Choosing a set first? See the best free icon libraries.

Packages, inline SVG or a sprite

Four ways to put icons in a React app
ApproachInstallTree-shakingBest for
lucide-reactnpm i lucide-reactYes, one module per iconA single consistent outline set
react-iconsnpm i react-iconsYes with a modern bundler; import from one family pathMixing icons from many sets
Inline SVG componentNothing; paste the SVG into a componentOnly what you writeA handful of custom or brand icons
SVG sprite + <use href>One sprite file you serveNot needed; the browser loads one fileMany icons repeated across a page, or non-React pages

Using each one

lucide-react

Each icon is a named export. size, color and strokeWidth are props, and any other prop is passed to the svg.

import { Camera, Search } from "lucide-react";

export function Toolbar() {
  return (
    <div className="toolbar">
      <Camera size={20} strokeWidth={1.75} aria-hidden="true" />
      <Search className="h-5 w-5 text-slate-500" aria-hidden="true" />
    </div>
  );
}

react-icons

Import from the family's own path (fa, md, lu, fi and so on). IconContext.Provider sets defaults for every icon inside it.

import { FaGithub } from "react-icons/fa";
import { MdEmail } from "react-icons/md";
import { IconContext } from "react-icons";

export function Footer() {
  return (
    <IconContext.Provider value={{ size: "1.25em", className: "footer-icon" }}>
      <a href="https://github.com/your-org" aria-label="GitHub"><FaGithub /></a>
      <a href="mailto:[email protected]" aria-label="Email us"><MdEmail /></a>
    </IconContext.Provider>
  );
}

Your own SVG component

Paste the path data once and reuse it like any component. width and height of 1em make it follow the text size.

export function CameraIcon({ title, ...props }) {
  return (
    <svg
      viewBox="0 0 24 24" width="1em" height="1em"
      fill="none" stroke="currentColor" strokeWidth={2}
      strokeLinecap="round" strokeLinejoin="round"
      role={title ? "img" : undefined}
      aria-hidden={title ? undefined : true}
      {...props}
    >
      {title && <title>{title}</title>}
      <path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" />
      <circle cx="12" cy="13" r="3" />
    </svg>
  );
}

Tree-shaking and bundle size

We bundled one-icon imports with esbuild in production mode and gzipped the result. Named imports from both packages stay small; a namespace import pulls in the whole set. React itself is not counted.

Measured size added by icon imports (minified, then gzip -9; React not counted)
ImportMinifiedGzipped
Inline SVG component (one camera icon)467 B355 B
lucide-react 0.462: 1 icon (Camera)1,545 B874 B
lucide-react 0.462: 5 icons2,172 B1,085 B
react-icons 5.7: 1 icon from react-icons/fa2,934 B1,345 B
react-icons 5.7: 1 icon from react-icons/lu2,828 B1,263 B
lucide-react 0.462: import * (every icon)603,577 B137,366 B

SVG sprites

An SVG sprite with <use href>

Define each icon once as a <symbol>, then draw it anywhere with <use>. Edit the colour on the wrapper and both icons follow, because the paths use currentColor.

<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <symbol id="i-check" viewBox="0 0 24 24">
    <path d="M20 6 9 17l-5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
  </symbol>
  <symbol id="i-x" viewBox="0 0 24 24">
    <path d="M18 6 6 18M6 6l12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
  </symbol>
</svg>

<p style="color:#4f98fb;font-size:28px">
  <svg width="1em" height="1em" aria-hidden="true"><use href="#i-check"/></svg>
  <svg width="1em" height="1em" aria-hidden="true"><use href="#i-x"/></svg>
</p>

Using a sprite file from React

Serve the symbols as /icons.svg and point each icon at an id in it. The browser downloads and caches the file once. An external sprite must be on the same origin.

export function Icon({ name, size = 20, label }) {
  return (
    <svg
      width={size} height={size}
      role={label ? "img" : undefined}
      aria-label={label}
      aria-hidden={label ? undefined : true}
    >
      <use href={`/icons.svg#${name}`} />
    </svg>
  );
}

// <Icon name="i-check" />  or  <Icon name="i-x" label="Close" />

Sizing, colour and accessibility

Size and colour with currentColor

Icons that use currentColor take the text colour of their parent, so hover and dark mode styles need no icon-specific CSS. Change the color value and watch both icons follow.

<style>
  .btn { color: #1f1f1f; font-size: 18px; display: inline-flex; gap: .4em; align-items: center;
         border: 1px solid #ddd; border-radius: 999px; padding: .4em .9em; background: #fff; }
  .btn:hover { color: #4f98fb; }
  .btn svg { width: 1em; height: 1em; }
</style>
<button class="btn">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
  Search
</button>

Decorative versus meaningful icons

An icon beside visible text is decoration: hide it. An icon that is the only content of a button needs a name on the button.

// Decorative: the text already says it
<button>
  <Trash2 aria-hidden="true" /> Delete
</button>

// Icon-only: label the button, hide the icon
<button aria-label="Delete project">
  <Trash2 aria-hidden="true" />
</button>

// A standalone icon that carries meaning
<AlertTriangle role="img" aria-label="Warning" />

Next.js notes

Next.js App Router

lucide-react and react-icons components render fine in Server Components because they are plain SVG with no state. Only the component that handles a click needs "use client".

// app/page.tsx (a Server Component)
import { ArrowRight } from "lucide-react";

export default function Page() {
  return (
    <a href="/start" className="cta">
      Get started <ArrowRight size={18} aria-hidden="true" />
    </a>
  );
}

Next.js: optimizePackageImports

Next.js already optimises imports for a list of common icon packages, including lucide-react and react-icons. Add any other icon package you use here so dev builds stay fast.

// next.config.js
module.exports = {
  experimental: {
    optimizePackageImports: ["my-icon-package"],
  },
};

Why imports matter

Both packages put each icon in its own module, so a bundler can drop everything you did not import. That only works with named imports. A namespace import of lucide-react measured 137 KB gzipped in our test, against under 1 KB for one icon.

Some older setups, such as Jest or a bundler without ES module support, can still load a whole family file. If a build is slow or large, check which file the icon import resolves to.

Common mistakes

Setting fill on an outline icon. Outline sets draw with stroke, so a fill colour shows as a solid blob.

Leaving icon-only buttons without a name, so screen readers say only "button".

Setting a width without a height in CSS on an svg that has no viewBox, which stretches it.

Loading an external sprite from a CDN on another domain. Browsers block cross-origin <use> references.

How it works

  1. Pick an approach from the comparison table.
  2. Install the package, or paste the SVG into a component or a sprite.
  3. Import each icon by name; never import the whole package as a namespace.
  4. Set size with a prop or 1em, and colour through the parent's text colour.
  5. Add aria-hidden to decorative icons and a label to icon-only buttons.

Tips

Frequently asked questions

Is lucide-react or react-icons smaller?

In our test both were small for a single named import: 874 bytes gzipped for one lucide-react icon and about 1.3 KB for one react-icons icon, because react-icons adds a small shared wrapper. Both grow by only a few hundred bytes per extra icon.

How do I change an icon's colour in React?

Most React icon components draw with currentColor, so set the CSS color on the icon or its parent, for example with a text colour class. lucide-react and react-icons also accept a color prop.

Should icons have alt text?

Only when the icon is the only thing conveying meaning. Then give it role="img" and an aria-label, or label the button that contains it. Icons next to visible text should be hidden with aria-hidden="true".

Do icon components work in Next.js Server Components?

Yes. lucide-react and react-icons components are stateless SVG and render on the server. You only need "use client" in a component that uses state or event handlers.

When is an SVG sprite better than a package?

When the same icons appear many times or on pages that are not React. Each <use> is a short reference to one cached file, rather than repeating the full path data in the HTML.

Related: Best free icon libraries, Favicon generator, HTML images, React Router examples, UI components, All free tools.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap