Introduction
Icons are everywhere in modern interfaces — navigation, actions, status indicators, and decoration. A well-built icon system provides consistent sizing, coloring, and accessibility while keeping your bundle small and your workflow smooth.
There are several approaches: inline SVGs, SVG sprites, icon fonts, and component-based systems. Each has trade-offs in performance, developer experience, and flexibility.
This guide covers building a React icon component system with SVGs, optimizing for bundle size, and ensuring icons are accessible.
Key Concepts
Icon Component Pattern
// components/Icon.tsx
import { type SVGProps } from 'react';
interface IconProps extends SVGProps<SVGSVGElement> {
size?: number | string;
label?: string; // Accessible label
}
export function Icon({ size = 24, label, children, ...props }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden={!label}
aria-label={label}
role={label ? 'img' : undefined}
{...props}
>
{children}
</svg>
);
}
Individual Icon Components
// icons/ChevronDown.tsx
import { Icon, type IconProps } from './Icon';
export function ChevronDown(props: IconProps) {
return (
<Icon {...props}>
<path d="m6 9 6 6 6-6" />
</Icon>
);
}
// icons/Search.tsx
export function Search(props: IconProps) {
return (
<Icon {...props}>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</Icon>
);
}
Practical Examples
1. SVG Sprite System
// public/icons.svg — sprite file
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="icon-search" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="8" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="m21 21-4.3-4.3" fill="none" stroke="currentColor" stroke-width="2"/>
</symbol>
<symbol id="icon-menu" viewBox="0 0 24 24">
<path d="M3 12h18M3 6h18M3 18h18" fill="none" stroke="currentColor" stroke-width="2"/>
</symbol>
</svg>
// Usage component
export function SpriteIcon({ name, size = 24, label }: { name: string; size?: number; label?: string }) {
return (
<svg width={size} height={size} aria-hidden={!label} aria-label={label}>
<use href={`/icons.svg#icon-${name}`} />
</svg>
);
}
2. SVGO Optimization Pipeline
// svgo.config.js
module.exports = {
plugins: [
'preset-default',
'removeDimensions',
{ name: 'removeAttrs', params: { attrs: '(fill|stroke)' } },
{ name: 'addAttributesToSVGElement', params: { attributes: [{ fill: 'currentColor' }] } },
],
};
// package.json script
// "optimize-icons": "svgo -f src/icons/raw -o src/icons/optimized"
3. Icon with Button
// Accessible icon button
export function IconButton({ icon: Icon, label, ...props }: IconButtonProps) {
return (
<button aria-label={label} className="p-2 rounded hover:bg-gray-100" {...props}>
<Icon size={20} aria-hidden />
</button>
);
}
<IconButton icon={Search} label="Search" onClick={openSearch} />
<IconButton icon={Menu} label="Open menu" onClick={toggleMenu} />
Best Practices
- ✅ Use currentColor for icon fill/stroke — inherits text color automatically
- ✅ Provide aria-label for standalone icons, aria-hidden for decorative ones
- ✅ Optimize SVGs with SVGO before including in your bundle
- ✅ Use consistent sizing — 16px, 20px, 24px matching your spacing scale
- ✅ Tree-shake icons — import individually, not from a barrel export of hundreds
- ❌ Don't use icon fonts — SVGs are more accessible, flexible, and crisp
- ❌ Don't embed complex illustrations as icon components — use <img> instead
Common Pitfalls
- Importing all icons at once — a barrel export of 500 icons bloats your bundle
- Forgetting accessibility — standalone icons need labels; decorative icons need aria-hidden
- Inconsistent viewBox sizes — mixing 20x20 and 24x24 icons causes alignment issues
- Hardcoded colors in SVG paths — prevents theming with currentColor
Related Guides
- Building Component Libraries — Icons as part of your component system
- Design Tokens Complete Guide — Icon sizing aligned with spacing tokens
- ARIA Attributes Guide — Accessible icon labeling
- React Performance Optimization — Tree-shaking icon imports