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 text | Icon stroke width (24px grid) |
|---|---|
| Regular (400), 14–16px | 1.5px |
| Medium/Semibold (500–600) | 2px |
| Bold (700), or emphasized standalone | 2.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
1em–1.25emwhen 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:
| Variant | Use for |
|---|---|
| Outline | Default state: toolbars, list rows, inline with text |
| Fill | Selected/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:
| Flip | Don't flip |
|---|---|
| Back/forward arrows, chevrons in navigation | Logos 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 glyphs | Media 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.