Drawing Icons with Claude

My first attempt at generating an icon set fell apart somewhere past fifty. This one held at 108. The model got better, but most of the difference was a file of rules, written one rejection at a time.

7 min

A while ago I tried to get a model to generate icons for the Lumx design system. The first few dozen were fine. Somewhere past fifty the set stopped being a set: weights drifted, corners disagreed, and every new icon matched the prompt while matching none of its neighbours. I put it down.

Galleri had the same problem from the other side: icons from three sources (a Lucide-style set, some Figma exports, a few drawn by hand) that had never been in a room together. With Opus 5.5 out, I gave it another go. This time it held, all 108 of them. The model is better at geometry, but that isn't most of why.

FIG 01Twelve places in Galleri. The top row came from three different sets; the bottom row is one family.

What broke last time

Consistency isn't a property of an icon. It's a relation between icons, and a prompt can't hold relations. Every generation decided the corner radius again, and the stroke, and how a line ends near a badge. Fifty icons is fifty chances to decide slightly differently, and the drift compounds, because each new icon is checked against a description rather than against the set.

So this time nothing was drawn alone, and nothing was decided twice.

Icons as specs

None of the icons is freehand path data. Each one is a spec on the 24 grid, built from shared helpers: rounded boxes, arcs that end in a head, lines that stop a measured distance short of a mark. A radius lives in a function, so it gets decided once.

family.js, simplified
const SIDE = {  folder: { s: [FOLDER_LINE], f: [FOLDER_TAB] },  favorites: { s: [RING], f: [star(12, 12.5, 5.25, 2.5)] },  files: { s: [BOX_OPEN, SLOT], f: [rr(2.5, 3, 19, 5, 1.75)] },};

s is stroked, f is filled, and that second key is the whole visual language.

One filled piece

The first sheet put five systems side by side on four sidebar icons: outline only, an outline with one filled part, a solid with the details cut out, and two that swapped the metaphor entirely. Every option was shown at 16px beside its real neighbours, in light and in dark, because an icon on its own tells you nothing about weight.

FIG 02The first sheet, redrawn. What Galleri had, then systems A to E. B won.

B came with a condition: the filled part has to be a real piece of the object. The mic's capsule, the bookmark's band, the bin's lid, the note's fold, the folder's tab. Never half a star, and never a band across a page because the page looked empty.

FIG 03Six of the 108. Each fill is something the object actually has.

That sentence did more for consistency than the grid did, because it hands the model a question to answer for every new icon: which part is this? When there was no good answer, the icon was wrong.

Every no is a rule

From there the loop was plain. Opus drew a sheet with two to five options per icon; I marked up a screenshot and sent it back. Thirty-nine sheets, ninety-six screenshots.

What made it scale is that every rejection got written down, with a number, in a guide, GUIDE.md, that the model reads before it touches an icon. Icon 90 obeys a note I left on icon 6. That file is the difference between this and Lumx.

The first smart folder cleared room for its sparkle with a circle. My note was to offset the secondary mark instead. In the guide it became: any cut is the mark's own outline grown by 1.75 units, and a line that meets a mark ends there, with a round cap.

FIG 04The same cut, three ways. The circle bites; the offset belongs to the sparkle; the shipped one just stops.

Inside corners went the same way. Where a fill meets a line, ink pools at 16px. The first fix used radii from 1 to 2.5 and inflated everything. The guide now says 0.45 to 0.6, with the reason next to the number.

FIG 05Judge it on the small ones. Half a unit clears the knot; anything more and the window goes soft.
FIG 06The note's fold. Where the fill meets the frame, the centre line turns through a short arc instead of a corner, so the ink stays softened at any weight.

Written rules also catch the model drifting. Halfway through, new icons stopped getting the softened corners. The note back was five words long, and from then on it was a line in the file rather than something either of us had to remember.

GUIDE.md, rejected and why
Tried                   Verdict         LessonInner radii 1 to 2.5    too round       0.45 to 0.6Circle around a mark    offset it       Grow its outlineHeads pasted on arcs    not one shape   Square to the tangentLeaf, coffee, rocket    hated           Retired, remappedVery round cube         (unprintable)   Hexagon, counters 0.6

Measured, not eyeballed

Most of those rules are distances, and distances can be computed. A line that stops short of a mark isn't trimmed by eye. The helper, trimDash, samples the mark, walks the line in steps of 0.05 units, keeps the points at least one stroke plus the gap away, and returns the result as a dash pattern on the original path.

family.js, simplified
function trimDash(d, marks, sw = SW) {  const D = sw + GAP;  const mark = marks.flatMap((m) => samples(m, 0.1).pts);  const { L, pts } = samples(d, 0.05);  const keep = pts.map(([x, y]) => nearest(mark, x, y) >= D);  return { d, dash: runs(keep), offset: L - firstKept(keep) };}

One path, round caps intact, and the cap lands exactly the gap away from the mark at any weight. Arrowheads on arcs work the same way: the head's base is centred on the arc's end and set square to the tangent there, so line and head read as one stroke instead of a triangle pasted on at a slightly wrong angle.

FIG 07Reload. The head's base is centred on the arc's end, square to the tangent there.

Weight is a measurement too. On screen, the stroke matches the stem of the Inter label beside it, 0.107em at weight 500, which is 1.5px next to a 14px label. Drawn at 16px in the sidebar that's 2.25 units; menus and the editor sit beside other labels, and get 2 and 2.75. The same icon carries a different weight in every place it's used, and the table lives in the guide with everything else.

Where taste ran out

The model's defaults are the average of every icon set it has seen. For the icons people pick for their own collections it offered a leaf, a coffee cup, a rocket, a shirt, a camera, a palette and a house: well drawn and completely wrong. Galleri's users are designers sorting references, and nobody files their references under coffee.

FIG 08The first draft of the collection icons, above the twelve that replaced it.

The set became web, mobile, layout, colour, illustration, shapes, branding, motion, sound, 3D, code and maps, all on the folder's skeleton. Typography was tried twice and dropped. That call had nothing to do with geometry. It's knowing who the app is for, and it's the one part of this I wouldn't hand over.

Review at ten times

For this post I asked for one blueprint card per icon, like the two above. They turned into the QA pass. A folder with one sharp counter corner and one radius off by half a unit. An arrow cut out of its circle by the outline alone, so the head came out hollow. A mountain meeting its frame at a sharp corner. None of it visible at 16px.

FIG 09One of the cards: the grid with its rulers, the keylines in grey, the ink, and the centre line with its anchors in blue.

icons.jsx is the only source. An export script outlines every stroke with Skia's path ops, unions the parts and subtracts the masks, so each icon also exists as one flat path for Figma. At Skia's default precision the curves came out flattened to a quarter unit, and one ring was visibly dented; at precision: 100 it's exact. A pixel diff at 240px now compares every stroked icon with its outlined twin, and a script flags any cut-out masked by its outline alone. Review that runs on every batch beats review that depends on someone squinting.

Why it held

FIG 10All 108, in the order the guide lists them.

Looking back at Lumx, the model wasn't the only thing missing. I asked it to be consistent and gave it nothing to be consistent with. Galleri's set had a memory: a 246-line guide, a helper for every decision that repeats, and a sheet that never showed an icon without its neighbours. Opus drew, the guide remembered, and I said no, precisely and in writing.

It isn't perfect, and I know it. Sort the set by ink, as above, and the swing from the solid tiles to the thin chevrons is wider than I'd like. Menus and the top bar still sit at 2 units while the small places moved to 2.25. There are corners I'd still nudge by hand.

But perfect was never what this round was for. What changed is the price of an option. Five complete systems for the same four icons is days of drawing by hand; here it was a request, and throwing four of them away cost nothing. Exploring stopped being the expensive part.

Next is the part that stays manual: a pass over the outlined exports in Figma for the details no rule reaches. Then the library grows, and the guide grows with it, so icon 150 obeys a note I left on icon 6.

And if I start Lumx again, I'll start with an empty GUIDE.md.