Icon system — a shared pictogram language

Icons multiply fast in a product: different packs, different sizes, raw SVGs with unique viewBoxes. An icon system restores order: one wrapper, one size scale, explicit a11y rules.

Choosing a library

Criteria:

  • Style — outline / filled / duotone; fit with the UI.
  • License — allowed in a commercial product.
  • Tree-shaking — import individual icons, not the whole set.
  • CustomizationcurrentColor, stroke-width.
  • API stability — rare breaking changes.

Popular options: Lucide, Heroicons, Phosphor, a custom set from Figma (SVGR). A DS often locks one primary set plus a process for adding custom brand icons.

Icon wrapper

Do not scatter <svg width="16"> across the codebase. Make an Icon:

const iconSizes = {
  xs: 12,
  sm: 16,
  md: 20,
  lg: 24,
  xl: 32,
};

export function Icon({ icon: Glyph, size = 'md', className, title, decorative = true, ...props }) {
  const px = iconSizes[size] ?? iconSizes.md;
  return (
    <Glyph
      width={px}
      height={px}
      className={cn('icon', className)}
      aria-hidden={decorative ? true : undefined}
      role={decorative ? undefined : 'img'}
      focusable="false"
      {...props}
    >
      {!decorative && title ? <title>{title}</title> : null}
    </Glyph>
  );
}

Wrapper rules:

  • Size only from the DS scale (sm/md/lg), not 13px.
  • Color via currentColor — the icon inherits text/button color.
  • focusable="false" on decorative SVGs in IE/older engines.
  • Alignment with text: inline-flex + a small optical offset when needed.

Decorative vs meaningful

TypeExampleA11y
Decorativeicon next to the text “Settings”aria-hidden="true"
Meaningful“error” status shown only as an iconaccessible name: aria-label / <title> / hidden text
Interactiveicon-button “Close”prefer <button aria-label="Close"> + decorative icon inside

If nearby visible text already carries the same meaning — the icon is decorative. If the icon carries meaning alone — give it a name.

// Decorative
<button type="button">
  <Icon icon={Settings} decorative />
  Settings
</button>

// Meaningful (status)
<span className="status">
  <Icon icon={AlertCircle} decorative={false} title="Validation error" />
</span>

SVG optimization

  • Run through SVGO (strip extra meta, fixed fills when you need currentColor).
  • Prefer component imports (import { Search } from 'lucide-react').
  • Do not put huge illustrations into the Icon system — those are illustrations.

Naming and catalog

Agree on a vocabulary: plus, close, chevron-down, search. Document a Storybook gallery of every icon with copyable names — that reduces “we have three different close icons.”

Practice

  1. Choose a pack and lock it in the DS README.
  2. Implement Icon with a size scale and decorative flag.
  3. Audit: icon-only buttons without labels are bugs.
  4. Add a gallery to Storybook.

An icon is not decoration — it is part of the interface language. Consistent size, color via tokens, and an explicit a11y policy matter more than “finding a pretty pack.”