Icon systems
Never mix icon packs. 10-32px range. Match stroke to font weight. Lucide is overused.
Raw markdown On GitHubreferences/components/icon-systems.md
Taught in
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.
componentsicons