How to Add Icons to a React App (The Complete 2026 Guide)

Updated: by DownloadIcons Team Development 10 min read

Adding icons to a React app sounds trivial — until you’re staring at a 400 KB bundle because you imported an entire icon font, or fighting a component that won’t take a className. There are really only three approaches worth using in 2026, and picking the right one for your project saves you from refactors later. This guide walks through all three with working code, then covers the bundle-size and accessibility details that actually matter.

Every icon on DownloadIcons downloads as clean, optimized SVG and can be exported directly as a ready-to-paste React (.tsx) component, so whichever route you take you’re starting from the right source.

The three approaches at a glance

ApproachBest forBundle impactCustomizable
First-party icon packageApps using one main libraryLow with tree-shakingProps for size/color/stroke
Your own SVG componentsA small, curated setTiny — only what you shipFull control
Shared sprite + <use>Hundreds of repeated iconsLowest at scaleColor/size via CSS

Approach 1: Use a first-party icon package

Most modern libraries publish a React package where each icon is a tree-shakeable component. This is the lowest-friction option and what most teams should reach for first. Lucide is the common default:

npm install lucide-react
import { Search, Settings, Heart } from 'lucide-react';

export function Toolbar() {
  return (
    <div className="toolbar">
      <Search size={20} />
      <Settings size={20} strokeWidth={1.5} />
      <Heart size={20} color="#ef4444" />
    </div>
  );
}

The big win is per-icon imports. Because you import Search rather than the whole library, a bundler like Vite, webpack, or Rollup tree-shakes away every icon you don’t use — your final bundle contains only the handful you actually render.

Other libraries follow the same pattern with their own package names:

Choose this when your app standardizes on one library and you want size/color/stroke props out of the box. For help picking the library itself, see best free icon libraries for 2026.

Approach 2: Build your own SVG components

If you only need a dozen icons — or you’re mixing glyphs from several sources — pulling in a whole package is overkill. Copy the SVG and wrap it in a component you control. On any icon page you can export the React component directly, but the pattern is simple enough to write by hand:

import type { SVGProps } from 'react';

export function SearchIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      {...props}
    >
      <circle cx="11" cy="11" r="8" />
      <path d="m21 21-4.3-4.3" />
    </svg>
  );
}

Two details make this component genuinely reusable:

  • stroke="currentColor" means the icon inherits the surrounding text color. Set color (or a Tailwind text-* class) on a parent and the icon follows — no per-theme assets.
  • Spreading {...props} lets every caller pass className, width, aria-label, or an onClick without you predicting them up front.

Choose this when you have a small, hand-picked set and want zero dependencies and total control over the markup. It’s also the most reliable way to combine icons from different libraries — just normalize them all to the same viewBox and stroke. The minimalist-line style is a good source of glyphs that already share a 24×24 grid.

Approach 3: A shared SVG sprite

When the same icons repeat across hundreds of rows — a data table, a long feed, a file browser — inlining the full <svg> markup every time bloats your HTML. A sprite defines each icon once and references it by ID:

// Sprite rendered once near the root of your app
<svg style={{ display: 'none' }} aria-hidden="true">
  <symbol id="i-search" viewBox="0 0 24 24" fill="none"
          stroke="currentColor" strokeWidth={2}>
    <circle cx="11" cy="11" r="8" />
    <path d="m21 21-4.3-4.3" />
  </symbol>
</svg>

// A tiny reusable component
function Icon({ name, size = 24, ...props }: { name: string; size?: number }) {
  return (
    <svg width={size} height={size} {...props}>
      <use href={`#i-${name}`} />
    </svg>
  );
}

The browser parses each <symbol> once and reuses it everywhere, so a list of 500 rows costs one definition instead of 500 copies of the path data. Modern bundlers can generate the sprite for you from a folder of SVGs.

Choose this when you render the same icons at scale and want the smallest possible runtime payload. The trade-off is slightly more setup and that styling is limited to what CSS can reach (color via currentColor, size via width/height).

Keep your bundle lean

The number-one icon mistake in React is shipping far more than you render. A few rules keep it tight:

  • Always use per-icon imports (import { Search } from 'lucide-react'), never import * as Icons. The wildcard defeats tree-shaking and can pull in thousands of icons.
  • Don’t load an icon font just to show 15 glyphs — that’s the whole font on every page load. The reasoning is laid out in SVG vs PNG vs icon fonts.
  • Lazy-load rarely-used icons. Icons that only appear in a settings modal can live behind a dynamic import() so they don’t weigh down first paint.
  • Audit with your bundler’s analyzer. If icons are a meaningful slice of your JS, switch heavy spots to the sprite approach.

Make your icons accessible

React doesn’t make icons accessible for you — the rules are the same as plain HTML, and they come down to one decision: is the icon decorative or meaningful?

{/* Decorative — text already says "Settings" */}
<button>
  <Settings aria-hidden="true" focusable="false" /> Settings
</button>

{/* Meaningful — icon is the only label */}
<button aria-label="Delete item">
  <Trash2 aria-hidden="true" />
</button>

An icon-only button with no aria-label is the single most common icon accessibility failure. Note that in JSX you write aria-hidden and aria-label with hyphens (they’re real DOM attributes), unlike camelCased props such as strokeWidth. For the full set of patterns — contrast, hit-area sizing, reduced motion — see the icon accessibility guide.

Frequently asked questions

Which import keeps my bundle smallest? Named, per-icon imports from a first-party package, or your own components — both ship only what you reference. Avoid wildcard imports.

Can I change color and stroke width per instance? Yes. With stroke="currentColor" the color follows text color (or a Tailwind class), and outline sets respond to a strokeWidth prop. You can preview color, size, and stroke on any icon page before exporting.

How do I add a whole set of icons at once? Use the kit builder to assemble a pack and export every icon as React components in one download, complete with an attribution file.

Do these work with Next.js / Vite / Remix? Yes — all three approaches are plain React and SSR-safe, since icons are just SVG markup with no browser-only APIs.

The bottom line

For most React apps, reach for a first-party icon package with per-icon imports — it’s the fastest path and tree-shakes cleanly. Drop to your own SVG components for a small curated set or when mixing sources, and use a sprite when the same icons repeat at scale. Whichever you choose, start from optimized SVGs: browse 21 libraries, customize, and export React components directly from any icon or pack.

Share this post

Related Posts