Documentation
Design guidelines
The rules every icon follows, the vocabulary they share, and the measurements behind both.
Icons here are not drawn in a vector editor. Each one is a TypeScript declaration —
a list of shapes on a 24-unit grid — and the build compiles it into all six cells:
outline and duotone, across thin, regular and bold. The shape constructors throw on
illegal geometry and pnpm icons:validate checks thirty-odd rules in under five
seconds, so everything mechanical on this page is enforced by machine. What follows is
the reasoning, which a validator cannot give you.
The grid
| Canvas | 24 × 24, viewBox="0 0 24 24" |
| Anchors | inside 2..22, multiples of 0.5 |
| Stroke | round cap and join; 1.5 / 2 / 2.5 by weight |
| Fill | none — duotone adds a 20% tint, never a solid |
| Angles | multiples of 45° |
| Size | an icon's longer side spans 18–22 units |
| Optical centre | within 2 units of the canvas centre |
| Crossings | at most 2 |
| Stroke gaps | ≥ 2.5 units apart (3 at bold), runs ≥ 2.5 |
Ink reaches 1..23 because a 2px stroke bleeds 1px past the axis.
Why the live area is that wide
The first version of this set capped anchors at 3..21, which limits any shape to a 20-unit span. Measured against comparable sets, ours reached 20 where theirs reached 22 — every icon was 10 to 26% smaller inside the same frame, and shapes that do not fill their frame read as timid.
Widening it also settled the stroke question. At 2px the set averages 28.7% ink coverage, which is where established sets sit. Pushing to 2.5px took seven of ten icons past 34% and turned them to mush at 16px. The confidence came from scale, not from a heavier line.
Why a weight is a drawing, not a multiplier
Thin, regular and bold share their geometry — the build proves it — but the rules tighten with weight: a gap that reads at 1.5px closes up at 2.5px, so the validator's minimum gaps grow with the stroke. Drawing once and checking three times is what keeps bold from becoming a smudged copy of regular.
The shared bodies
A concept's outer shape is decided by what kind of thing it is, and the bodies live in
one file (scripts/draw/bodies.ts) so families cannot drift apart:
| Body | Meaning |
|---|---|
| Ring (gapped circle) | An agent — autonomous, has state |
| Chamfered frame | A machine or MCP server — gets invoked |
| Plug with prongs | A tool or capability |
| Page with a folded corner | A document or resource |
| Cylinder | Stored data |
| Shield | Security |
| Window frame | An interface, a page, a sandbox |
| Square loop with a chevron | Rotation and repetition — refresh, retry, lifecycle |
The mark inside the body says which member of the family this is: agent is the
bare ring, agent-run holds a triangle, agent-stop a square, mcp-tool-call puts a
small arrow in the plug's hollow. The body is byte-identical across a family — that is
what makes the family read as one.
Corners and terminals
Caps and joins are round. Corner radius follows the size of the shape carrying it: 2 units at 8 and over, 1 below, because a 2-unit radius on a 5-unit box leaves almost no straight side and the shape stops reading as a box.
An earlier version cut every container's corners at 45° instead, as a house signature. It
was dropped. A signature made of a corner treatment governs the container and then fights
the metaphor — vector-database drawn with cut corners stopped reading as a database at
all. The set is distinguished by its vocabulary and by having six cells where most sets
have one, not by a cut that has to be argued for on every shape.
Duotone is derived, not drawn
Nobody draws the duotone cell. The build tints every closed shape at 20% opacity behind the strokes; an icon made only of open strokes gets a halo instead — the same paths echoed behind it, three units wider, at the same 20%. Small closed marks (a status dot) tint without suppressing the halo. One drawing, and the second variant can never disagree with the first.
Make the inner mark large
This is measured, not taste. The first agent-thinking was a circle holding three 2px
dots, and agent was the same circle holding one — perceptually 0.011 apart, which
is indistinguishable. Enlarging the dots to 4px and spreading them across 40% of the
width moved it to 0.059.
A small mark in the middle of a large shape distinguishes nothing at 16px, however different it looks at 48px.
What actually kills legibility at 16px
Not element count. An icon with five elements stays sharp as long as its strokes do not
cross; one with four elements and four crossings becomes a dark blot. That is why
max-crossings is a rule and the twelve-element limit is left loose.
The other killer is fusion: two strokes closer than the gap minimum melt together at bold. When a composition will not fit — two arrowheads and a shaft between two bodies, say — the answer is to redesign the layout (route the arrows around the middle), not to shave the gaps.
Before you open a pull request
Four things the validator cannot judge:
- Can you guess the meaning without a label?
- Does it sit with its neighbours in the same subcategory?
- Is it readable at 16px?
- Does the outer shape match the entity type?