# Iconography

> Use when choosing or drawing icons: stroke weight, pixel grid, optical sizing, icon-versus-label, and morphing one icon into another.

- Skill: `agentsorg/iconography` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add agentsorg/iconography`
- Raw SKILL.md: https://api.skillmd.com/api/skills/agentsorg/iconography/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: agentsorg (https://skillmd.com/u/agentsorg)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/agentsorg/iconography

---



# Drawing and Choosing Icons

Default posture: one icon set for the whole product, one effective stroke weight per surface, matched to the weight of the text it sits beside, and a label on anything whose metaphor the platform has not already taught. Icons change size by changing *cut*, not by scaling a single drawing — a 24px outline mark shrunk with CSS keeps detail that was never meant to survive at 12px, and its stroke lands off the pixel grid. **This skill owns the structural constraint that makes one icon able to become another — matched primitive counts, the three-line budget, rotation groups — while the duration, curve and interruption behaviour of that morph belong to `transitions`, and whether it animates at all is decided by the frequency gate before either skill is loaded.** Photographs, illustration, avatars and device frames are `imagery`.

**Read which set the project already installs before drawing or importing anything.** Check `package.json` for `lucide-react`, `@radix-ui/react-icons`, `@phosphor-icons/react`, `@heroicons/react`, `material-symbols`, or a local `components/icons` directory, and stay inside it. Adding a second set is the fastest way to make an interface look assembled rather than designed: two sets differ in grid, stroke, corner radius and terminal style, and those differences compound across a card grid until every row reads slightly wrong for a reason nobody can name.

## Quick Reference

| Open this | When |
| --- | --- |
| [icon-sets.md](references/icon-sets.md) | You need a set's grid, default stroke or size cuts, or you are matching a new icon to an existing set's weight. |
| [morphing.md](references/morphing.md) | One icon must become another — menu to close, chevron rotation, play to pause — and you need the shape constraints that make it possible. |

## Core Principles

1. **One set per product, and it is the set already installed.** Stroke weight, terminal style and corner radius are set-wide decisions; mixing two sets re-litigates all three on every surface. Import from the existing package. *Exception:* the product's own logo and third-party brand marks (GitHub, Google, Slack) are never part of the icon set, are never restyled to match it, and are never redrawn.
2. **Match the effective stroke to the adjacent text weight.** A hairline icon beside semibold text reads as a mistake, and the number that matters is the rendered stroke, not the declared one: `effective px = stroke-width × (rendered px ÷ viewBox units)`. Lucide's default `stroke-width: 2` in a `24` box renders `1.33px` at `16px`. House default: text at weight `400–500` wants a `1.5px` effective stroke; `600` and above wants `2px`. *Exception:* filled sets such as Radix carry no stroke — match those by visual mass against a sample, since there is no number to compute.
3. **Change the cut when the size changes; do not scale one drawing.** Detail drawn for `24px` turns to mush at `12px`, and a `1px` stroke disappears into antialiasing. Below roughly `12px`, switch to the filled variant — Heroicons ships separate `20×20` and `16×16` solid sets for exactly this reason. *Exception:* variable icon fonts retune themselves; set Material Symbols' `opsz` axis (`20`–`48`) instead of swapping files.
4. **Snap the drawn geometry to whole pixels at the size it will actually render.** Half-pixel coordinates blur on 1× screens, and the blur reads as low quality without ever being identifiable. Radix Icons are drawn on a `15` grid, so they are half a pixel off at `16px` — render them at `15px`. *Exception:* icons only ever shown at `32px` and above, or icons that are always in motion, where subpixel movement defeats hinting anyway.
5. **Optical size is not box size — align by mass, not by box.** A play triangle centred mathematically in a circle sits visibly left-heavy because its mass is on the left; nudge it about `1px` toward the point. *Exception:* a row or grid of icons from one set already shares padding and alignment by construction; per-icon nudges there create the misalignment they were meant to fix.
6. **An icon may stand alone only if the mark is already learned, the action is repeated, and the control is named to assistive tech.** The floppy disk means nothing to anyone under thirty. Give the label to everything else, and give every icon-only control an `aria-label` naming the *action*: `aria-label="Search"`, never `aria-label="icon"`. *Exception:* the marks the platform has already taught — close, search, menu, back, add, overflow — which carry meaning without text.
7. **Give an icon-plus-label pair a `6–8px` gap.** With no breathing room the two collide into one blob; with too much they read as separate controls. *Exception:* a compact density mode may drop to `4px`, but only when the whole row's rhythm drops with it — `spacing` owns the density scale.
8. **One icon, one meaning, product-wide.** If a chevron means both "expand this accordion" and "go to the next page", users reliably learn neither; if a star means both "favourite" and "rating", the state is ambiguous. Pick a distinct mark for the second meaning. *Exception:* rotation groups — a chevron at `0deg`, `90deg`, `180deg` and `270deg` is one meaning (disclosure) pointed four ways, and that is a legitimate reuse.
9. **Icons that will ever morph share a primitive budget from the moment they are drawn.** Benji Taylor's icon set gives every icon exactly three SVG lines, collapsing the spares to invisible points where an icon needs fewer, which is what makes any icon in the set able to become any other. Retrofitting that onto arbitrary paths does not work. *Exception:* shapes that cannot be reconciled — play to pause — crossfade or swap; forcing a morph there produces a smear.
10. **Union multipath icons into one path with one fill, coloured by `currentColor`.** Multiple fills mean multiple things to theme, to invert for dark mode, and to break. *Exception:* duotone and two-tone sets, where the second layer is the design — declare both layers as `currentColor` with an opacity rather than hardcoding a grey that will not survive a theme change.

## Smell / Fix

| Smell | Fix |
| --- | --- |
| Two icons on one card look like different products | Two sets mixed — delete one and reimport from the survivor |
| Icons look spindly beside the label | Effective stroke below the text weight — compute it and raise `stroke-width` |
| A small icon looks blurry on a non-retina screen | Off-grid geometry at that size — render at the set's native grid or snap coordinates |
| A 16px icon is unreadable mush | A 24px drawing scaled down — switch to the small cut or the filled variant |
| Play button looks off-centre inside its circle | Mathematical centring — nudge ~`1px` toward the point |
| Icon-only button, and nobody knows what it does | Unlearned metaphor — add a visible label |
| Screen reader announces "button" with no name | Missing `aria-label` naming the action |
| Chevron means expand here and next-page there | One mark, two meanings — give navigation a distinct arrow |
| Icon and label look collided | Gap under `6px` |
| Icon stays black in dark mode | Hardcoded fill — switch to `currentColor` |
| A morph smears mid-transition | Mismatched primitive counts — reconcile the shapes or crossfade |
| The morph plays on every keystroke in a palette | Frequency gate violation — swap instantly at 100+/day |

## Output Format

When handing back a drawn or corrected icon, ship the SVG itself with `viewBox`, `stroke-width`, `stroke-linecap` and `stroke-linejoin` explicit, `fill="none"` and `stroke="currentColor"` for outline sets, no `width`/`height` attributes (the consumer sizes it), and one line stating which render sizes it is cut for and its effective stroke at each. When auditing a set rather than drawing one, report per surface: which set each icon came from, its effective stroke, and the adjacent text weight.

## Checklist

- [ ] Every icon on the surface comes from one set
- [ ] Effective stroke computed and matched to adjacent text weight
- [ ] Small sizes use the small cut or the filled variant, not a scaled-down drawing
- [ ] Geometry lands on whole pixels at the intended render size
- [ ] Optically centred where the mark's mass is off-centre
- [ ] Every icon-only control has an `aria-label` naming the action
- [ ] Icon-plus-label gap is `6–8px`
- [ ] No mark carries two meanings
- [ ] `currentColor`, single path, single fill; no hardcoded hex
- [ ] `viewBox` present, `width`/`height` absent
- [ ] Any morphing pair shares a primitive count; timing deferred to `transitions`

