# One Icon Scale, One Stroke Width

**SHOULD** · **ID:** `design-icon-size-scale` · **Category:** design
**Source:** [RAMS](https://www.rams.ai/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/design-icon-size-scale

> SHOULD: Bind icon sizes to ONE scale (16 / 20 / 24, matched to the type scale — `size-4` next to `text-sm`, `size-5` next to `text-base`) and hold the whole set to ONE stroke width, which in practice means taking every glyph from a single icon family. A toolbar sized by eye (`w-4`, `w-5`, `size-[18px]`, `h-6`) reads as ragged even when every box is aligned, because the eye compares the drawn marks and not the boxes; and a 1.5px stroke beside a 2px stroke gives two apparent weights at the same nominal size. Sizes come from tokens — a lone `size-[18px]` is a magic number.

Bind icon sizes to the type scale and hold every icon to a single stroke width, instead of sizing each one by eye

The third clause of that bullet — the one this corpus kept dropping. design-rams-border-radius covers "borders" and design-rams-shadow-consistency covers "shadows"; this is "icon sizing", and the explanation is ours. Icons are the one element people size by eye, so a toolbar ends up at w-4, w-5, size-[18px], h-6 — every glyph centered, every box aligned, and the row still reads as ragged, because the eye compares the drawn marks and not the boxes around them. Stroke width is the half nobody checks: a 1.5px stroke next to a 2px stroke at the same nominal size gives you two apparent weights, so one icon looks bolder than its neighbour for no reason anyone can name. Fix both with two decisions. One SIZE scale — 16 / 20 / 24, bound to the type scale so an icon matches the text it sits beside (size-4 next to text-sm, size-5 next to text-base) — and one STROKE width across the whole set, which in practice means taking every glyph from a single icon family rather than mixing libraries. Sizes come from tokens, not from arbitrary values: a lone size-[18px] is a magic number that the next person will guess wrong.

## Bad — do not do this

`design-icon-size-scale-bad`

```tsx
/**
 * Bad: a toolbar assembled one icon at a time. Five glyphs at w-4, w-5, size-[18px],
 * h-6 and w-5, with stroke widths drifting between 1.5 and 2.5. Every button is
 * centered and every box aligns — and the row still reads as ragged, because the eye
 * compares the drawn marks, not the boxes around them.
 */
type Glyph = { label: string; box: string; stroke: number; d: string };

const TOOLS: Glyph[] = [
  { label: 'Bold', box: 'w-4 h-4', stroke: 2.5, d: 'M6 4h6a4 4 0 0 1 0 8H6zM6 12h7a4 4 0 0 1 0 8H6z' },
  { label: 'Link', box: 'w-5 h-5', stroke: 1.5, d: 'M10 14a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1' },
  { label: 'Quote', box: 'w-[18px] h-[18px]', stroke: 2, d: 'M7 6H4v6h5V6H7zm10 0h-3v6h5V6h-2z' },
  { label: 'List', box: 'w-6 h-6', stroke: 1.5, d: 'M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01' },
  { label: 'Code', box: 'w-5 h-5', stroke: 2.25, d: 'M8 18l-6-6 6-6M16 6l6 6-6 6' },
];

export function IconSizeScaleBad() {
  return (
    <div className="w-full max-w-sm space-y-3">
      <p className="text-xs text-muted-foreground">Editor toolbar &middot; each icon sized by eye</p>

      <div className="flex items-center gap-1 rounded-lg border border-border bg-card p-2">
        {TOOLS.map((tool) => (
          <button
            key={tool.label}
            type="button"
            aria-label={tool.label}
            className="flex size-9 items-center justify-center rounded-md text-foreground hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
          >
            <svg
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              strokeWidth={tool.stroke}
              strokeLinecap="round"
              strokeLinejoin="round"
              aria-hidden="true"
              className={tool.box}
            >
              <path d={tool.d} />
            </svg>
          </button>
        ))}
      </div>

      <div className="space-y-1 rounded bg-muted p-2 font-mono text-xs">
        <code className="block text-error">sizes: w-4, w-5, w-[18px], w-6, w-5</code>
        <code className="block text-error">strokes: 2.5, 1.5, 2, 1.5, 2.25</code>
      </div>

      <p className="text-xs text-error">
        Optically ragged even though every glyph is perfectly centered in a 36px button. The
        sizes come from four different decisions and the strokes from five, so Bold looks heavy,
        Link looks faint, and nothing in the row belongs to anything else. The lone{' '}
        <code className="font-mono">w-[18px]</code> is a magic number the next person will guess wrong.
      </p>
    </div>
  );
}
```

## Good — do this

`design-icon-size-scale-good`

```tsx
/**
 * Good: two decisions instead of ten. ONE stroke width across the whole set, and ONE
 * size scale (16 / 20 / 24 → size-4 / size-5 / size-6) bound to the type scale, so an
 * icon is sized by the text it sits beside rather than by eye.
 */
const STROKE = 1.75;

type Glyph = { label: string; d: string };

const TOOLS: Glyph[] = [
  { label: 'Bold', d: 'M6 4h6a4 4 0 0 1 0 8H6zM6 12h7a4 4 0 0 1 0 8H6z' },
  { label: 'Link', d: 'M10 14a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1' },
  { label: 'Quote', d: 'M7 6H4v6h5V6H7zm10 0h-3v6h5V6h-2z' },
  { label: 'List', d: 'M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01' },
  { label: 'Code', d: 'M8 18l-6-6 6-6M16 6l6 6-6 6' },
];

function Icon({ d, className }: { d: string; className: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={STROKE}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d={d} />
    </svg>
  );
}

export function IconSizeScaleGood() {
  return (
    <div className="w-full max-w-sm space-y-3">
      <p className="text-xs text-muted-foreground">Editor toolbar &middot; one scale, one stroke</p>

      <div className="flex items-center gap-1 rounded-lg border border-border bg-card p-2">
        {TOOLS.map((tool) => (
          <button
            key={tool.label}
            type="button"
            aria-label={tool.label}
            className="flex size-9 items-center justify-center rounded-md text-foreground hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
          >
            {/* Toolbar controls all take the same step of the scale */}
            <Icon d={tool.d} className="size-5" />
          </button>
        ))}
      </div>

      {/* Icons sized to the text they sit beside: size-4 next to text-sm, size-6 next to text-lg */}
      <div className="space-y-2 rounded-lg border border-border bg-card p-3">
        <p className="flex items-center gap-2 text-lg font-semibold text-foreground">
          <Icon d={TOOLS[4].d} className="size-6 shrink-0" />
          Snippets
        </p>
        <p className="flex items-center gap-2 text-sm text-muted-foreground">
          <Icon d={TOOLS[3].d} className="size-4 shrink-0" />
          Four items in this collection
        </p>
      </div>

      <div className="space-y-1 rounded bg-muted p-2 font-mono text-xs">
        <code className="block">scale: size-4 (16) / size-5 (20) / size-6 (24)</code>
        <code className="block">stroke: {STROKE} everywhere</code>
      </div>

      <p className="text-xs text-success">
        One stroke width means every glyph carries the same apparent weight, so nothing looks
        bolder than its neighbour by accident. One size scale, bound to the type scale, means the
        icon size is derived rather than chosen &mdash; there is no 18px to guess at.
      </p>
    </div>
  );
}
```

## References

- [Rams design review skill](https://rams.ai/rams.md)
- [MDN: stroke-width](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/stroke-width)
- [Tailwind: width](https://tailwindcss.com/docs/width)
