Skip to main content
Technology & EngineeringDesign Systems214 lines

Icon Systems

Icon systems, SVG management, and scalable icon delivery pipelines for design systems

Quick Summary33 lines
You are an expert in icon systems and SVG management for building and maintaining design systems.

## Key Points

- **Viewport size** — typically 24x24 or 20x20
- **Stroke width** — consistent across the set (e.g., 1.5px or 2px)
- **Padding** — 1-2px live area inset from the viewport edge
- **Style** — outlined, filled, or duo-tone (pick one primary style, offer variants)
- Use `currentColor` for stroke or fill so icons inherit the parent text color automatically and work seamlessly with theming.
- Automate the SVG-to-component pipeline in CI so designers can drop new SVGs into a folder and the icon library updates without manual work.
- Provide an accessible `label` prop — when set, render `role="img"` with `aria-label`; when absent, mark the icon as `aria-hidden="true"`.
- Shipping icons with hard-coded colors (e.g., `fill="#000"`) which break in dark mode and prevent consumers from styling them.
- Neglecting to optimize SVGs before bundling, resulting in bloated markup with editor metadata, hidden layers, and unnecessary precision in path data.

## Quick Example

```
icon-arrow-left.svg
icon-check-circle.svg
icon-chevron-down.svg
icon-file-text.svg
icon-user-plus.svg
```

```ts
// src/icons/index.ts
export { ArrowLeftIcon } from './ArrowLeftIcon';
export { CheckCircleIcon } from './CheckCircleIcon';
export { ChevronDownIcon } from './ChevronDownIcon';
// ...auto-generated
```
skilldb get design-systems-skills/Icon SystemsFull skill: 214 lines
Paste into your CLAUDE.md or agent config

Icon Systems — Design Systems

You are an expert in icon systems and SVG management for building and maintaining design systems.

Overview

An icon system provides a consistent, performant, and maintainable way to deliver hundreds of icons across a product. It covers the pipeline from designer-authored SVGs through optimization, packaging, and runtime rendering, ensuring every icon aligns to the same grid, stroke weight, and sizing conventions.

Core Philosophy

An icon system is infrastructure, not a folder of SVG files. The difference is the pipeline — a well-designed icon system takes raw designer-authored SVGs and automatically optimizes, normalizes, and packages them into consumable components. Designers drop files into a directory, CI runs the pipeline, and engineers import tree-shakeable icon components without touching SVG markup by hand.

Visual consistency is the primary value an icon system provides. When every icon shares the same viewport, stroke width, optical weight, and padding, they feel like they belong together regardless of who drew them. A 24x24 grid with 1.5px strokes and 2px padding is a common convention, but the specific values matter less than universal adherence to them. A single icon that uses 2px strokes in a set of 1.5px icons is immediately visible and undermines the entire system.

Icons must be color-agnostic by default. Using currentColor for stroke or fill ensures icons inherit their parent's text color, which makes them work seamlessly with dark mode, brand themes, and contextual coloring (success green, error red) without any icon-specific styling. Hardcoded colors in SVG source files are the single most common icon system defect.

Core Concepts

Icon Grid and Consistency

All icons in a system should share:

  • Viewport size — typically 24x24 or 20x20
  • Stroke width — consistent across the set (e.g., 1.5px or 2px)
  • Padding — 1-2px live area inset from the viewport edge
  • Style — outlined, filled, or duo-tone (pick one primary style, offer variants)

Delivery Strategies

StrategyDescriptionBest for
Inline SVG componentsEach icon is a React/Vue componentTree-shaking, SSR
SVG sprite sheetSingle <svg> with <symbol> defsHTTP/1 sites, HTML-only
Icon fontGlyphs mapped to Unicode codepointsLegacy support only
Dynamic importLazy-load icons by nameLarge icon sets, code splitting

Naming Conventions

Use a flat, descriptive naming scheme:

icon-arrow-left.svg
icon-check-circle.svg
icon-chevron-down.svg
icon-file-text.svg
icon-user-plus.svg

Prefix with icon-, use lowercase kebab-case, and avoid abbreviations.

Implementation Patterns

SVG Optimization Pipeline with SVGO

// svgo.config.js
module.exports = {
  multipass: true,
  plugins: [
    'preset-default',
    'removeDimensions',
    {
      name: 'addAttributesToSVGElement',
      params: {
        attributes: [
          { fill: 'none' },
          { stroke: 'currentColor' },
          { 'stroke-width': '1.5' },
          { 'stroke-linecap': 'round' },
          { 'stroke-linejoin': 'round' },
        ],
      },
    },
    {
      name: 'removeAttrs',
      params: { attrs: ['class', 'data-name', 'id'] },
    },
  ],
};

Code Generation: SVG to React Component

// scripts/generate-icons.js
const fs = require('fs');
const path = require('path');
const { optimize } = require('svgo');

const SVG_DIR = path.join(__dirname, '../assets/icons');
const OUT_DIR = path.join(__dirname, '../src/icons');

const toPascalCase = (str) =>
  str.replace(/(^|-)(\w)/g, (_, __, c) => c.toUpperCase());

fs.readdirSync(SVG_DIR)
  .filter((f) => f.endsWith('.svg'))
  .forEach((file) => {
    const name = toPascalCase(file.replace('icon-', '').replace('.svg', ''));
    const raw = fs.readFileSync(path.join(SVG_DIR, file), 'utf8');
    const { data } = optimize(raw);

    const svgBody = data
      .replace(/<svg[^>]*>/, '')
      .replace('</svg>', '');

    const component = `import type { SVGProps } from 'react';

export function ${name}Icon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.5}
      strokeLinecap="round"
      strokeLinejoin="round"
      width="1em"
      height="1em"
      {...props}
    >
      ${svgBody.trim()}
    </svg>
  );
}
`;
    fs.writeFileSync(path.join(OUT_DIR, `${name}Icon.tsx`), component);
  });

Wrapper Icon Component

import type { SVGProps } from 'react';

export interface IconProps extends SVGProps<SVGSVGElement> {
  /** Icon size in px or rem — applied to width and height */
  size?: number | string;
  /** Accessible label; if omitted the icon is decorative (aria-hidden) */
  label?: string;
}

export function Icon({
  size = '1.25em',
  label,
  children,
  ...props
}: IconProps) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      width={size}
      height={size}
      fill="none"
      stroke="currentColor"
      strokeWidth={1.5}
      role={label ? 'img' : 'presentation'}
      aria-label={label}
      aria-hidden={label ? undefined : true}
      {...props}
    >
      {children}
    </svg>
  );
}

Barrel Export with Tree-Shaking

// src/icons/index.ts
export { ArrowLeftIcon } from './ArrowLeftIcon';
export { CheckCircleIcon } from './CheckCircleIcon';
export { ChevronDownIcon } from './ChevronDownIcon';
// ...auto-generated

Consumers import only what they use:

import { CheckCircleIcon } from '@acme/icons';

Best Practices

  • Use currentColor for stroke or fill so icons inherit the parent text color automatically and work seamlessly with theming.
  • Automate the SVG-to-component pipeline in CI so designers can drop new SVGs into a folder and the icon library updates without manual work.
  • Provide an accessible label prop — when set, render role="img" with aria-label; when absent, mark the icon as aria-hidden="true".

Common Pitfalls

  • Shipping icons with hard-coded colors (e.g., fill="#000") which break in dark mode and prevent consumers from styling them.
  • Neglecting to optimize SVGs before bundling, resulting in bloated markup with editor metadata, hidden layers, and unnecessary precision in path data.

Anti-Patterns

  • Icon fonts in new projects. Icon fonts were a reasonable solution in the HTTP/1 era, but they have significant downsides: no tree-shaking, rendering as text (which affects accessibility), and FOIT (flash of invisible text) during loading. Use inline SVG components for all new work.

  • Hardcoded colors in SVG source files. Icons with fill="#000000" or stroke="#333" baked into the paths cannot adapt to dark mode, brand themes, or contextual coloring. Every icon should use currentColor exclusively so it inherits its color from the parent element.

  • No optimization pipeline. Shipping SVGs straight from Figma or Illustrator includes editor metadata, unnecessary groups, hidden layers, and excessive decimal precision in path data. This bloats the icon bundle and produces inconsistent markup across icons.

  • Inconsistent viewport sizes. Mixing 16x16, 20x20, and 24x24 icons in the same system forces consumers to remember which icon uses which size and manually adjust. Standardize on one viewport (or a clearly defined set with explicit size variants) and enforce it in the pipeline.

  • Missing accessibility handling. Icons used as standalone interactive elements (like an icon-only button) need an accessible label via aria-label. Decorative icons alongside text need aria-hidden="true". An icon system that does not provide a label prop and corresponding ARIA handling leaves accessibility to chance.

Install this skill directly: skilldb add design-systems-skills

Get CLI access →