Product design skills

icons.md

A supporting file of the better-ui skill.

Icons

Icon weight, states, sizing and direction, the details that make icons sit naturally in an interface.

Match icon stroke to text weight

A hairline icon beside semibold text reads as broken; a heavy icon beside regular text shouts.

Adjacent textIcon stroke width (24px grid)
Regular (400), 14–16px1.5px
Medium/Semibold (500–600)2px
Bold (700), or emphasized standalone2.5px
<!-- Good: stroke tuned to the label weight -->
<button class="flex items-center gap-2 font-semibold">
  <PlusIcon stroke-width="2" class="size-4" />
  New project
</button>

<!-- Bad: default 1.5px stroke against a bold label -->
<button class="flex items-center gap-2 font-bold">
  <PlusIcon stroke-width="1.5" class="size-4" />
  New project
</button>

Two related consistency rules:

  • One optical strategy per surface. Never mix icon libraries with incompatible stroke conventions on one toolbar. Where the library supports stroke variants, match them to adjacent text as above; otherwise keep the set's native stroke and use size or color for emphasis.
  • Size icons relative to the text's cap height, typically 1em1.25em when inline with text, so the pair scales together.

One SVG, recolored per state

Never ship separate assets for default, hover, selected and disabled states. Use one SVG drawn with currentColor and let CSS state drive the color:

<!-- Good: one asset, states are CSS -->
<svg fill="none" stroke="currentColor" stroke-width="2">…</svg>
.icon-button { color: oklch(0.552 0.016 285.938); }
.icon-button:hover { color: oklch(0.21 0.006 285.885); }
.icon-button[aria-pressed="true"] { color: oklch(0.623 0.188 259.815); }
.icon-button:disabled { opacity: 0.4; }
<!-- Tailwind -->
<button class="text-zinc-500 hover:text-zinc-900 aria-pressed:text-blue-600 disabled:opacity-40">
  <BookmarkIcon />
</button>

Hardcoded fills inside the SVG, such as fill="#666", break this. Strip them to currentColor when importing icons.

Outline default, fill active

Where an icon set offers outline and filled variants, use them as a state pair, never interchangeably:

VariantUse for
OutlineDefault state: toolbars, list rows, inline with text
FillSelected/active state: the active tab, a toggled bookmark, a liked heart
// Good: variant communicates state
<TabIcon variant={isActive ? "solid" : "outline"} />

// Bad: filled icons everywhere, so the active tab has no state signal
<TabIcon variant="solid" />

The swap between variants is a contextual icon animation. Use the exact cross-fade values in icon-transitions.md.

Design at render size

An icon that looks great at 48px collapses into mush at 16px. Thin interior lines, tight counters and fine texture all blur or alias when small.

  • Test every icon at the smallest size it will render, often 16px. It must stay recognizable there.
  • Prefer simplified glyphs for small contexts over scaling down detailed artwork.
  • Keep icons on the pixel grid at their render size. A 16px icon drawn on a 24px grid with fractional scaling renders soft, so use the set's native grid sizes (16, 20, 24) rather than arbitrary scales.
  • Always SVG, never raster, so the same asset stays crisp at every density.

Icons in RTL

Under dir="rtl", flip icons whose meaning is tied to reading direction, and leave the rest alone:

FlipDon't flip
Back/forward arrows, chevrons in navigationLogos and brand marks
Text-block glyphs (alignment, lists, indent)Checkmarks
Speaker/volume waves (emanate in reading direction)Physical objects: clocks, cups, pencils
"Send" style directional glyphsMedia playback (play/rewind refer to tape direction, convention keeps them LTR)
/* Good: mirror only direction-dependent icons */
[dir="rtl"] .icon-directional {
  scale: -1 1;
}
<!-- Tailwind -->
<ChevronRightIcon class="icon-directional rtl:-scale-x-100" />

Analyze composite icons part by part. A badge or slash overlay may keep its position even when the base glyph flips. Accessible names for icon-only buttons belong to better-accessibility.

On this page