---
title: icon-systems
summary: Never mix icon packs. 10-32px range. Match stroke to font weight. Lucide is overused.
tags: [components, icons]
---

# Icon systems

Icons are the most replicated UI element in a product. They are also the easiest to get wrong via mismatch — three icons from three different packs at three different stroke widths in the same UI.

## Pick one icon pack

The single most important rule: **never mix icon packs**. The eye notices instantly, even if the user can't articulate why the UI feels off.

If you started with Material Icons and now want a feature that needs a custom icon, draw the custom icon to match Material's style (rounded, filled, 24px viewbox). Don't reach for Heroicons "just for this one."

To fetch icons, `better-icons` searches Iconify's sets from the CLI or an MCP server. Always pass `--prefix <your pack>`: its similar-icon tool and usage ranking pull from other packs, which this rule forbids. It never shows a set's license, so check the license yourself. Then run the [[svg-creation]] cleanup.

## Packs worth considering

| Pack | Style | Best for |
|---|---|---|
| **Lucide** | Outline, 24×24 | Solid default but overused — pick Phosphor or Hugeicons if you want differentiation |
| **Phosphor** | Multi-weight outline | More personality than Lucide, same coverage |
| **Hugeicons** | Wide library | Less common, distinctive |
| **Tabler** | Outline, 1.5px stroke | Cleaner than Lucide at small sizes |
| **Iconoir** | Outline, hand-drawn feel | Editorial / blog energy |
| **Heroicons** | Outline + solid pairs | Tailwind ecosystem |
| **Material Symbols** | Filled, variable | Apps that already use Material |

## Sizing

| Use | Size |
|---|---|
| Inline with body text | 16px |
| Toolbar buttons | 20px |
| Primary navigation | 20–24px |
| Hero / empty state | 32–48px |
| Marketing | 24–96px |

Below 14px, most icon packs lose detail. Above 32px, hand-drawn or larger-viewBox icons fare better than 24px-viewBox stretched up.

## Match stroke weight to font weight

If your font is **500 (Medium)**, your outline icons should have a **1.75–2px stroke** at 24px.
If your font is **400 (Regular)**, your icons should have a **1.5px stroke**.

Phosphor's "regular" weight is 1.5px; "bold" is 2.5px. Pick the one that matches your type.

## Icon transformations, not crossfades

When toggling an icon (e.g., chevron expand/collapse, play/pause, sun/moon), **rotate or transform** rather than crossfade between two icons. See [[fly-not-teleport]].

- Chevron up/down: rotate 180°.
- Play/pause: replace via opacity *but* if you have the bandwidth, draw a single morphing path.
- Sun/moon: morph via shared geometry (lucide-animated.com has examples).

See [[animations-dev-curriculum]] for the canonical resource on icon animation.

## Drawing and meaning

- Below ~12px, swap outline icons for the filled variant; 1px strokes vanish. Snap coordinates to whole pixels so 1× screens don't blur.
- Round caps by default; square caps read unfinished. Union multipath icons into one path with one fill.
- Don't upscale: a 16px icon at 48px keeps its spindly stroke. Redraw it heavier. Favicons get a simplified mark hinted for 16px ([[svg-creation]]).
- One icon, one meaning: a chevron can't mean both "expand" and "next page"; a star can't mean both favourite and rating (heart for favourite).
- Pairs that differ only by a 180° turn (upload/download) need a second cue, such as a label or a baseline bar.
- 6–8px between an icon and its label; ambiguous metaphors (the floppy disk) get the text label.

## Illustration cohesion

Illustrations obey the icon system: same stroke weight, about three colours recoloured to the UI's tokens, light falling from the same side as the UI's shadows, one perspective per scene, negative space drawn in rather than padded on afterwards. Complexity matches the spot (a tooltip gets a small mark, not a scene), and they get redrawn, not scaled, for larger slots. Generic blob-and-character packs read as no budget; a typographic treatment or nothing beats them.

## When to apply

- Setting up a new design system: pick the pack, document it, lock it.
- Reviewing a PR that introduces a new icon: check the pack source.
- Auditing an existing UI: grep for `<svg>` elements from different packs.

## Gotcha

Icon stroke is `stroke-width`, not `border-width`. Setting `stroke-width: 2` on a 16px icon yields a different visual weight than 2px on a 24px icon (the smaller icon looks heavier). Either scale stroke proportionally or use a variable-weight icon pack like Phosphor.

## Sources

- guidelines.sh — "Never mix icon packs; match stroke to font weight; Lucide is overused."
- lucide-animated.com — icon animation patterns.
- Phosphor Icons — weight system documentation.
- Emil Kowalski's design-engineering practice, distilled by HKTITAN — the drawing, meaning and illustration rules.
- better-auth/better-icons (MIT; npm 1.0.4 on 2026-10-08, the repo's 1.0.5 is unpublished) — Iconify search and retrieval; the cross-pack pull is HKTITAN's reading of its tools.
