Skip to content
Icons

React Icons: Icon Set Guide

One import surface for thirty icon sets, from Font Awesome to Lucide, shipped as tree-shakeable React components with no font files and no sprite sheets.

TypeScript MIT
8.6M weekly downloads
12.7k GitHub stars
2.1 kB min+gzip bundle size
v5.7.0 latest version

Figures above measured on from the npm registry (latest version, last-week downloads) and the GitHub API (stars). Not copied from the project's own README, and re-measured rather than edited by hand, so the date tells you exactly how old these numbers are.

How the bundle size was measured. We installed react-icons@5.7.0 on its own, bundled export * from 'react-icons' with esbuild (esm, browser, minified, production), marked React external because your app already has it, and gzipped the result. Measured . This is the ceiling, not the typical cost: it imports the entire public surface with no tree-shaking credit. Pull one component from a well-shaken library and you'll pay a fraction of it. We publish the ceiling because it's the number we can reproduce, and reproducing it is the point.

iconssvgtree-shakingfont-awesomeheroicons

A real, running React Icons demo, not a screenshot -- every variant documented below is interactive.

Maintenance Health

Who keeps React Icons alive, and how it behaves when something breaks -- measured from the npm registry and the package's own repository, not the project's README.

Who maintains this
5 npm publishers • Volunteer team
Last npm release
Last repo activity
Open issues
244
Tree-shakeable
Yes -- no side effects declared
React peer range
*

TL;DR: React Icons repackages roughly thirty icon families as React components behind one dependency. Import from the family's subpath so tree-shaking works, size and color follow CSS by default, and nothing loads a font file. Browser: react-icons.github.io/react-icons.

What is React Icons?

React Icons is a build of many popular icon sets into a single npm package, with each set behind its own import path. Font Awesome, Material Design, Bootstrap, Feather, Heroicons, Lucide, Simple Icons, Phosphor, Remix and others are all present, and each icon arrives as an ordinary React component rendering inline SVG.

The design goal is that adding an icon costs one import and roughly a kilobyte, rather than a font file, a sprite sheet, or a build step.

When to use it

Use it when a product needs icons from more than one family. That happens more often than it sounds: a general UI set for the interface, plus brand marks for third-party logins, plus a technical set for file types. Installing three packages and reconciling three prop APIs is worse than one.

Use something else when you know your family. If the design system says Lucide, lucide-react gives you the same icons with a smaller dependency graph and faster type checking, because your editor isn't indexing thirty sets.

Key Features

  • Around thirty icon families behind one dependency
  • Inline SVG components, so no font loading and no FOUT
  • 1em sizing and currentColor fill, so icons follow surrounding text
  • IconContext.Provider for setting size, color and className across a subtree
  • Per-set import paths that let bundlers drop everything you don't use
  • Types included, so every icon is a typed component

Installation

npm install react-icons

Import from the two-letter subpath of the set, never from the package root:

import { FaGithub } from 'react-icons/fa';
import { LuSettings } from 'react-icons/lu';

Using Icons

import { FiTrash2, FiEdit2 } from 'react-icons/fi';

export function RowActions({ onEdit, onDelete }: {
  onEdit: () => void;
  onDelete: () => void;
}) {
  return (
    <div className="row-actions">
      <button onClick={onEdit} aria-label="Edit row">
        <FiEdit2 />
      </button>
      <button onClick={onDelete} aria-label="Delete row">
        <FiTrash2 />
      </button>
    </div>
  );
}

Both buttons carry the accessible name. The icons stay silent, which is what you want when the control already says what it does.

Setting Defaults For a Subtree

import { IconContext } from 'react-icons';

export function Toolbar({ children }: { children: React.ReactNode }) {
  return (
    <IconContext.Provider value={{ size: '1.25rem', className: 'toolbar-icon' }}>
      {children}
    </IconContext.Provider>
  );
}

This is the cleanest way to keep icon sizing consistent without repeating a prop on every instance.

TypeScript Tips

Every icon is an IconType, which is a function component taking IconBaseProps. That makes icons easy to pass around as data:

import type { IconType } from 'react-icons';
import { FiHome, FiUsers } from 'react-icons/fi';

type NavItem = { label: string; href: string; icon: IconType };

const nav: NavItem[] = [
  { label: 'Home', href: '/', icon: FiHome },
  { label: 'Team', href: '/team/', icon: FiUsers },
];

Render with <item.icon />. Typing the field as IconType rather than ReactNode keeps the component uninstantiated, so the consumer decides its size.

How do you style icons from CSS?

The icons render as inline SVG with stroke="currentColor", either fill="currentColor" or fill="none", and a height and width of 1em. So the two properties you actually use are font-size for size and color for colour, and both inherit like they do for text.

.icon-button {
  display: inline-grid;
  place-items: center;
  min-inline-size: 2.25rem;
  min-block-size: 2.25rem;
  font-size: 1.25rem;
  color: var(--ink);
  transition: color 120ms ease;
}

.icon-button:hover {
  color: var(--accent);
}

Rendering a few families with react-icons 5.7.0 and reading the markup shows that they aren't built the same way:

FamilyHow it paintsWeight control
Font Awesome (fa), Material (md), Bootstrap (bs), Phosphor (pi), Remix (ri)fill="currentColor", stroke-width="0"none, the shapes are solid outlines
Feather (fi), Lucide (lu), Tabler (tb)fill="none", stroke="currentColor", stroke-width="2", round caps and joinsstrokeWidth prop
Heroicons 2 outline (hi2)fill="none", stroke="currentColor", stroke-width="1.5"strokeWidth prop

Two things follow. A line icon with a CSS fill: red rule turns into a filled blob, so colour line icons through color or stroke and leave fill alone. And mixing a stroke family with a solid family in one toolbar gives you visibly different weights, which no prop will fix; that's the real reason to pick a primary set.

Two prop traps are worth knowing. The color prop doesn't only set a colour: it writes an inline style="color:...", and an inline style beats your .icon-button:hover rule, so the hover never shows. Leave color off and let it inherit. The size prop writes height and width attributes, which means font-size stops controlling the icon; use size or font-size for a given component, not both.

When you need to set something on every icon below a point in the tree, IconContext.Provider takes size, color, className, style and attr, where attr is merged onto the <svg> element.

How do you make icons accessible?

Out of the box, they aren't, and the defaults aren't even consistent. Rendering FiTrash2 or FaGithub gives you an <svg> with no aria-hidden, no role and no title. Rendering HiOutlineTrash from the Heroicons 2 set gives you the same <svg> with aria-hidden="true" already on it. Same package, same import style, different behaviour.

Three situations cover almost everything:

SituationWhat to doAccessible name
Icon next to visible texthide the icon: aria-hidden="true"the text
Icon is the only content of a buttonlabel the button: aria-label="Delete row"the label
Icon stands alone as an imagerole="img" and aria-label on the iconthe label

I checked these with dom-accessibility-api, the same name computation Testing Library uses, in jsdom. An icon-only button with nothing on it computes to an empty name, which is the failure you want to avoid. A title prop does produce a name, since it renders a <title> child, and aria-label on the control did too. I haven't run it through NVDA or VoiceOver here, and screen readers treat SVG <title> less consistently than a label on the control, so I'd put the label on the button and keep title for the tooltip.

The package won't enforce any of this, so enforce it once in a wrapper:

import type { IconType } from 'react-icons'

type IconProps = { icon: IconType; label?: string }

export function Icon({ icon: Glyph, label }: IconProps) {
  if (label) {
    return <Glyph role="img" aria-label={label} />
  }
  return <Glyph aria-hidden="true" focusable="false" />
}

An icon without a label is hidden, an icon with one is announced, and a developer has to choose. Used as <Icon icon={FiTrash2} label="Delete row" /> it computed to "Delete row", and <Icon icon={HiOutlineTrash} /> Delete to "Delete".

A shortcut tempts people here: attr={{ 'aria-hidden': 'true' }} on an IconContext.Provider hides every icon below it. That works for a toolbar whose buttons all carry text, and it silently hides every meaningful standalone icon in the same subtree.

Two WCAG 2.2 criteria matter beyond the name. Non-text contrast (1.4.11) asks for a 3:1 contrast ratio for icons that carry information, so a pale grey currentColor on white fails even though it looks tasteful. And target size (2.5.8, AA) asks for at least 24 by 24 CSS pixels for a pointer target, with a few exceptions such as well-spaced small targets. A bare 1em icon at 16px fails that by itself, which is what the min-inline-size and min-block-size in the CSS above are for.

How do you check tree-shaking actually worked?

Importing from the subpath is the rule, and rules you cannot verify get broken silently during a refactor. So measure it once.

npx vite build
npx source-map-explorer 'dist/assets/*.js'

What you want to see is a handful of tiny SVG component functions, each a few hundred bytes. What tells you something went wrong is a react-icons block measured in hundreds of kilobytes, which means a barrel import survived somewhere.

The fastest way to find the offender without reading the whole map:

grep -rn "from 'react-icons'" src/

Anything that match returns is a barrel import. The subpath version ends in the set, from 'react-icons/fa6', and that one character of difference is the whole optimisation. I'd add that grep to a pre-commit hook on any project with more than two developers, because it costs nothing and it catches the mistake at the only moment it is cheap to fix.

When should you use a dedicated package instead?

React Icons earns its place when you genuinely draw from several families, or when you do not yet know which family you will settle on. It stops earning it the moment you do know.

SituationBetter choice
Prototyping, family undecidedReact Icons
Mixing two or three families deliberatelyReact Icons
Settled on one familythat family's own package
Need consistent stroke weight across the whole UIa single set such as Lucide

The trade is editor performance and API surface against convenience. Thirty sets of type declarations is a real cost in a large TypeScript project, and it shows up as sluggish autocomplete rather than as an error, which makes it easy to blame on something else. Once your design has settled on one family, the dedicated package gives you faster tooling and a smaller dependency for the same rendered output.

My rule: start here, migrate out when the design system stops changing its mind. Migrating is a find-and-replace on import paths, so the cost of starting with React Icons and leaving later is genuinely low. That asymmetry is why it is the right default rather than a compromise.

Before you pick a primary set, run five checks against the shortlist: does it cover the icons you'll actually need (search for ten, not one); is the stroke weight consistent across the set; does the import path tree-shake in your bundler (the measurement above); what accessibility defaults does it ship, given the aria-hidden difference between families; and what does it cost in editor responsiveness. Only the last two are invisible in a demo, which is why they decide more migrations than they should.

Common Gotchas

The big one is the barrel import. import { FaGithub } from 'react-icons' looks equivalent to the subpath version and is not; depending on your bundler it can pull far more than one icon. Always import from the set.

The second is editor performance. Thirty sets is a lot of type declarations, and autocomplete in a large project can get sluggish. If you have settled on one family, moving to that family's own package is the fix.

The third is duplicated concepts across sets. Nothing stops you using FaTrash in one component and FiTrash2 in another, and the mismatch is visible. Pick a primary set and treat the others as exceptions.

Frequently Asked Questions

Does installing react-icons put every icon set in my bundle?

No, as long as you import from the set's subpath. `import { FaGithub } from 'react-icons/fa'` pulls that one component; the package is structured so bundlers can drop the rest. What does hurt is a barrel import from 'react-icons' itself, which some bundler configurations fail to shake.

How do I control the size and color of an icon?

Both inherit from CSS by default: the SVG uses `1em` for size and `currentColor` for fill, so an icon matches the font-size and color of its parent. Override per icon with the size and color props, or set them for a subtree with IconContext.Provider.

How is this different from installing an icon set directly?

A single set installed directly is usually the better choice - lucide-react for Lucide, @heroicons/react for Heroicons. React Icons earns its place when you need icons from several families in one product, or when you don't yet know which family you'll settle on and want to avoid rewriting imports later.

Are the icons accessible by default?

Mostly not, and it varies by family. Most sets render plain SVG with no accessible name and no aria-hidden, while the Heroicons v2 outline set adds aria-hidden=true itself. Decoration next to a text label should be hidden, and an icon that is the only content of a control needs a name: put aria-label on the button, or pass a title prop so the icon renders a <title> element.