How to Use SVG Icons in React the Right Way

Every React team eventually asks the same question—should icons be inline SVG, imported files, a sprite, or components from a library? The answer shapes bundle size, styling, and accessibility for the life of the project. This guide compares each approach with working code, shows how to color and size icons with CSS, and explains how to keep a thousand-icon library from bloating your bundle.
How to Use SVG Icons in React the Right Way

Icons look like a small decision until the codebase has three hundred of them. Then the choice you made in week one—inline markup, imported files, a sprite sheet, or a component library—determines how easy it is to change a color in dark mode, how much dead weight ships in your bundle, and whether screen readers announce nonsense to your users. Using SVG icons in React well is less about any single technique and more about picking the approach that matches how your product will grow.

This guide walks through every practical way to render SVG icons in a React application, compares the trade-offs honestly, and shows the patterns that keep icons styleable, accessible, and fast.

Why SVG Won the Icon Format War

Before SVG became the default, interfaces shipped icons as PNG sprites and icon fonts. Both are effectively legacy today, and the reasons explain what to look for in a modern setup.

Raster sprites blur on high-density displays and require a new image for every color variant. Icon fonts scale cleanly but come with real costs: they render as text (so failed font loads produce broken characters), anti-aliasing can make strokes look soft, and accessibility tooling struggles with private-use Unicode characters.

SVG has none of these problems. It is resolution-independent, styleable with plain CSS, animatable, and it lives in the DOM where you can attach ARIA attributes properly. The only real question left is how to get SVG markup into your React components—and that is where teams diverge.

The Four Ways to Render an SVG Icon in React

1. Inline SVG in JSX

The most direct approach is pasting the SVG markup straight into a component:

function HeartIcon() {
  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"
    >
      <path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
    </svg>
  );
}

Inline SVG gives you full CSS control—stroke="currentColor" means the icon inherits the surrounding text color automatically, which is the single most useful trick in icon styling. The downside is maintenance. Hand-pasted markup drifts: one icon gets stroke-width="2", another 1.5, attribute casing gets mistranslated from HTML to JSX, and updating an icon means finding and replacing raw paths.

Inline SVG is fine for one or two bespoke icons, like a custom logo mark. It does not scale to an icon system.

2. Importing SVG Files with SVGR

SVGR converts .svg files into React components at build time, and it powers the default SVG handling in many toolchains:

import HeartIcon from './icons/heart.svg?react';

function LikeButton() {
  return (
    <button>
      <HeartIcon aria-hidden="true" />
      Like
    </button>
  );
}

This keeps icon source files as plain SVG—designers can export from Figma, developers import the file—while still producing real components you can pass props to. The catch is configuration. The import syntax differs between Vite, webpack, and Next.js, and you own the pipeline: cleaning exported SVGs (stray fill attributes, editor metadata, fixed width and height) becomes your team’s job. SVGO helps, but someone has to maintain the config.

SVGR is the right choice when your product uses genuinely custom iconography that no library provides.

3. SVG Sprites with <use>

A sprite collects every icon into one SVG file of <symbol> elements, referenced by ID:

function Icon({ name, size = 24 }) {
  return (
    <svg width={size} height={size} aria-hidden="true">
      <use href={`/sprite.svg#${name}`} />
    </svg>
  );
}

<Icon name="heart" />

Sprites shine in one specific scenario: pages that render hundreds or thousands of icon instances. Because the browser parses each symbol once and references it, the DOM stays light and the sprite file caches independently of your JavaScript bundle. The trade-offs are a build step to generate the sprite, no per-instance access to the icon’s internal nodes, and icon names as strings—so a typo renders nothing and TypeScript cannot catch it.

4. An Icon Component Library

For most products, the practical answer is a maintained icon library with first-class React support. With Tabler Icons—free and open source, with over 5,900 icons drawn on a consistent 24×24 grid—the setup is one install:

npm install @tabler/icons-react
import { IconHeart, IconSettings, IconUser } from '@tabler/icons-react';

function Toolbar() {
  return (
    <nav>
      <IconHeart size={20} stroke={1.5} />
      <IconSettings size={20} stroke={1.5} />
      <IconUser size={20} stroke={1.5} />
    </nav>
  );
}

Every icon is a typed component, so autocomplete surfaces the full set and a misspelled name fails at compile time instead of rendering an empty box. Consistency comes free: every icon shares the same grid, default stroke width, and prop API, which is exactly the discipline that hand-managed icon folders lose over time. The React package documentation covers the full prop reference.

A library also solves the problem you have not hit yet: the forty-seventh icon. With inline SVG or SVGR, each new icon is a small sourcing-and-cleanup task. With a library, it is one more import.

Styling Icons: Color, Size, and Stroke

However icons get into your app, styling works best when you lean on inheritance instead of hardcoding.

Color. Stroke-based icons that use currentColor inherit the CSS color of their parent. This means an icon inside a danger button turns red with zero icon-specific CSS, and dark mode works automatically wherever your text colors already adapt:

<span style={{ color: 'var(--color-danger)' }}>
  <IconTrash aria-hidden="true" />
  Delete
</span>

If you find yourself passing explicit hex colors to individual icons, it is usually a sign the surrounding component should own the color instead.

Size. Match icon size to the text it accompanies—roughly 1em to 1.25em for inline use, commonly 16–20px in buttons and 24px standalone. Resist mixing arbitrary sizes across the interface; two or three sanctioned sizes keep screens visually calm.

Stroke width. Outline icon sets expose stroke width as a prop. Thinner strokes (1.25–1.5) read as lighter and more refined at large sizes; the default 2 holds up better at small sizes. Pick one value per context and apply it everywhere—mixed stroke widths side by side are one of the fastest ways to make a UI feel unpolished.

Keeping the Bundle Small

The most common fear about icon libraries—“won’t importing from a 5,900-icon package bloat my bundle?”—has a clear answer: not if imports are tree-shakeable, and modern icon packages are. When you import IconHeart, bundlers include that one component, not the library.

There are two real pitfalls to avoid:

Dynamic icon names defeat tree-shaking. Code like Icons[iconName] after importing the whole namespace forces the bundler to keep every icon, because it cannot know which ones you need at build time. If icon names truly come from data—a CMS, user configuration—build an explicit map of the icons your product actually uses:

import { IconHome, IconChartBar, IconSettings } from '@tabler/icons-react';

const NAV_ICONS = {
  home: IconHome,
  analytics: IconChartBar,
  settings: IconSettings,
};

function NavIcon({ name }) {
  const Icon = NAV_ICONS[name];
  return Icon ? <Icon size={20} aria-hidden="true" /> : null;
}

Barrel-file re-exports can drag in more than you use. If your internal components/index.js re-exports an icon-heavy module, some bundler configurations lose the ability to shake it. Importing icons directly where they are used avoids the issue entirely.

For genuinely icon-dense screens—an icon picker, a big data table with per-row status icons—the sprite approach from earlier can complement a component library rather than replace it.

Accessibility: Decorative vs. Meaningful

Every icon in your interface is one of two things, and each needs different markup.

Decorative icons sit next to text that already carries the meaning—an icon beside a “Settings” label adds nothing for a screen reader. Hide them:

<button>
  <IconSettings aria-hidden="true" />
  Settings
</button>

Meaningful icons stand alone, most often in icon-only buttons. The icon stays hidden from assistive technology, and the accessible name goes on the interactive element:

<button aria-label="Delete item">
  <IconTrash aria-hidden="true" />
</button>

The mistake to avoid is the middle ground: an icon-only control with no label at all, or an aria-label pasted onto the SVG instead of the button. Audit your icon-only buttons first—they are where most icon accessibility bugs live.

A Practical Default

If you are starting a React project today, the setup that serves most teams is:

  1. A component icon library as the base. Consistent grid, typed imports, tree-shakeable—Tabler Icons for React is free, MIT-licensed, and covers the overwhelming majority of interface needs.
  2. currentColor everywhere. Let icons inherit color from their context so theming and dark mode stay in one place.
  3. SVGR for the handful of custom icons your brand genuinely needs, cleaned through SVGO and drawn on the same grid as your library so they sit next to library icons without looking foreign.
  4. aria-hidden on decorative icons, aria-label on icon-only controls, enforced in code review or a lint rule.

Icons are one of the highest-repetition elements in any interface—the same small decisions made hundreds of times. Make those decisions once, encode them in components and conventions, and the rest of the product inherits the consistency for free. If you want to see this approach applied across a full interface, the Tabler admin template uses this exact icon system throughout every screen and component.

Related guides

Choosing an MIT Licensed Dashboard TemplateChoose an MIT licensed dashboard template with responsive layouts, reusable components, and practical freedom to customize, ship, and scale your apps.
Bootstrap Authentication Page Template GuideBuild a fast Bootstrap authentication page template with responsive layouts, accessible forms, secure flow states, and Sass-ready customization options.
10 Dashboard Template Examples That WorkSee dashboard template examples for SaaS, analytics, admin tools, and operations. Learn which layouts, charts, and tables fit each job with confidence.
emails banner imageemails banner image dark
NEW VERSION!
80 eye-catching, customizable email templates
ReactIconsSVG