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.
| If you want | Use |
|---|---|
| One clean, consistent set | lucide-react |
| Icons from many sets | react-icons |
| Two or three custom icons | An inline SVG component |
| The same icons repeated a lot | An 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.
| Approach | Install | Tree-shaking | Best for |
|---|---|---|---|
| lucide-react | npm i lucide-react | Yes, one module per icon | A single consistent outline set |
| react-icons | npm i react-icons | Yes with a modern bundler; import from one family path | Mixing icons from many sets |
| Inline SVG component | Nothing; paste the SVG into a component | Only what you write | A handful of custom or brand icons |
| SVG sprite + <use href> | One sprite file you serve | Not needed; the browser loads one file | Many icons repeated across a page, or non-React pages |
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>
);
}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>
);
}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>
);
}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.
| Import | Minified | Gzipped |
|---|---|---|
| Inline SVG component (one camera icon) | 467 B | 355 B |
| lucide-react 0.462: 1 icon (Camera) | 1,545 B | 874 B |
| lucide-react 0.462: 5 icons | 2,172 B | 1,085 B |
| react-icons 5.7: 1 icon from react-icons/fa | 2,934 B | 1,345 B |
| react-icons 5.7: 1 icon from react-icons/lu | 2,828 B | 1,263 B |
| lucide-react 0.462: import * (every icon) | 603,577 B | 137,366 B |
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>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" />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>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" />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 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"],
},
};
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.
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.
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.
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.
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".
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 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.
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.