Skip to content
Design

Icon Systems Guide

SVG sprites, icon components, and optimization — build a scalable icon system for your app.

stellae.design

8 min read

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

typescript
// 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

typescript
// 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

html
// 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

javascript
// 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

typescript
// 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

Designdesign

Was this article helpful?