1---
2name: ui-craft
3description: Make UI look and move right by measuring instead of guessing. Covers spacing, padding, margins and alignment, corners, borders and joins, motion and animation, layout shift, colour, theming and dark mode, icons, density, and native-looking platform chrome. Use this whenever you write or tune CSS, styling or any visual detail, an animation, a theme or an icon; whenever you match a reference app's or the platform's look; and whenever someone says something looks off, janky, jittery, cheap, cramped or "too web", points at a margin, a padding or a corner, or says it doesn't feel native.
4---
5
6# UI craft
7
8The examples come from Clover's apps: Snowbound (a OneNote 2010 remake), Clover
9Chat (a chat multiplexer) and the snow globe dashboard (a home-server console).
10The quotes are hers. Visual problems were the most common correction across all
11three. Most extra rounds came from two habits: guessing a geometry, timing or
12platform behaviour that could have been measured, and fixing one instance
13without checking the others.
14
15> "it's worth comparing this to computer use on how exactly textedit works
16> instead of a guess." — Clover, Snowbound
17
18## 1. Measure the reference first
19
20- **Know your reference.** It might be the app being remade (OneNote 2010 for
21 Snowbound), the platform's own apps and controls, a named inspiration (File
22 Pilot's motion, iMessage's bubbles), or the owner's earlier apps.
23- **Observe the real thing.** Use a disposable VM clone of the reference app,
24 computer use inside a sandbox, or native captures at 100%. Measure angles,
25 radii, offsets, colours, timings and font metrics. Snowbound's section tabs
26 lean at 45° because that angle was measured, not guessed.
27- **Run the inspiration app and use it yourself** rather than watching videos
28 ("i think its really worth getting a feel for this yourself in the
29 application"). Verify remembered behaviour before copying it: a list
30 animation Clover remembered from File Pilot didn't exist ("i saw it in a
31 dream~").
32- **Never answer from memory.** Mac OS X 10.6 window corners recalled from
33 memory were wrong: "finder and address book are rounded on bottom. safari,
34 settings, mail are squared".
35- **Inventory more than content:** container padding, resize handles, when
36 objects appear and disappear, scroll bounds, snapping. Matching only the text
37 cost Snowbound four rounds on its text box chrome.
38- **Record the resolved font next to every measurement.** A silent font
39 fallback skewed one of the measurements Snowbound's fidelity checks relied
40 on.
41- **Inject input the way hardware does.** Synthetic key events changed
42 OneNote's behaviour, so confirm a surprising result a second way.
43- **Report side by side at the same scale**, the reference on the left and
44 yours on the right. Snowbound's approvals followed these images ("this is
45 peak").
46
47## 2. Native first
48
49Let the platform own what people know by hand: file pickers, alerts, the caret
50and selection colours, editing chords, window frames and traffic lights,
51scrollbars, text interaction on iOS, and system materials.
52
53- **Use the platform's mechanism, not an imitation of it:**
54 - system vibrancy or Mica rather than a sampled colour ("it's almost
55 certainly a similar material to Mica and not just a color");
56 - server-side window decorations where the desktop provides them, as Ghostty
57 does;
58 - native UI fonts with optical sizing (a missing optical size was the main
59 density bug in Clover Chat's native port);
60 - ⌘+/−/0 for zoom, actions on key-down, the native live resize and zoom, and
61 overlay scrollbars.
62- **Imitate only after measuring, and only if it can match exactly.** "to sell
63 the illution they would have to match *identically* and if they do not then
64 it's not worth it." A hand-drawn GNOME close button set off a friend's "ui
65 smelling noise".
66- **Never draw fake OS chrome inside your own canvas.** Never paint over a
67 native material, and never dim one with a scrim.
68
69## 3. Motion
70
71- **Instant or visible, never in between.** "its not instant but not really
72 visible" reads as jitter.
73- **Content that changes because of typing or filtering swaps instantly.**
74 Animate opening and closing, not content: "worse than the instant switch".
75- **Popups open on press, and keys act on key-down.**
76- **One element, one clock.** Morph from the source's exact bounds, and never
77 cross-fade two copies of the same control: "i want the text box to instantly
78 appear at the bounds of the old one, *then animate*". Render a moving popup as
79 one layer clipped to its live outline, with rows already at their final
80 positions. Coupled values, like width and scale, share one curve.
81- **Only the participants move**: "ideally opening replies just moves the ones
82 involved." Motion should show where things came from.
83- **Animate only the property that changes.** A tab rises; its silhouette
84 doesn't grow. Where items overlap mid-motion, change colour instead of alpha,
85 "so that overlapping items dont double opacity".
86- **Animate on user toggles only, never on re-show.** Reopening a sidebar must
87 not replay its expand animation.
88- **Defaults from Clover's apps** (the owner's own taste wins; see section 10):
89 - File Pilot's feel: fast exponential easing that is frame-rate independent
90 and retargetable.
91 - Popup height: 150 ms (Clover approved this value).
92 - Spatial reveals such as reply threads: a harder, longer ease-out of about
93 500 ms.
94 - Every frame at the display's refresh rate: "no real excuse to drop frames
95 on a note taking app".
96- **Write a motion spec as numbers and anchors:** start scale, tilt, duration,
97 curve, anchor edge, what clips and what fades. Restate it before building, as
98 `ui-review-loop` describes.
99- **Judge motion live at real speed** (see `ux-testing`). Slowed frame strips
100 only supplement it.
101
102## 4. Nothing moves unless it means to
103
104- **State changes never move content.** Hover, selection and active styling
105 keep text at its x position: "keep the x position of the text intact". A
106 toggle doesn't move itself or its neighbours. A status indicator never changes
107 a label's weight or width; Snowbound shows unread as a dot in the gutter, not
108 as bold text. Load identity fields together, because a username swapping to a
109 display name is a layout shift.
110- **Adjust paint, not layout**: "cut down the padding of the two adjacent items
111 to make the gap bigger, not doing actual shift of the layout. really subtle."
112- **Hit targets have no gaps**: "the click target should not have a gap at
113 all." A visual gap insets the paint, never the hit box, and hover covers the
114 whole cell.
115- **Reserve space for anything that loads late**, such as images and charts,
116 and measure any shift in pixels.
117
118## 5. Pixels
119
120- **Keep radii concentric.** The inner radius equals the outer radius minus the
121 inset. Let the outer radius overscan so system rounding can't show through.
122- **Draw each shape as one silhouette** for its shadow, fill and rim.
123 Overlapping pieces seam where their antialiasing meets. Clover Chat's grouped
124 bubbles and its window corners both did.
125- **Single pixels count**: "the top left round has to go a single pixel more to
126 the left lol."
127- **After any visual fix, check every instance.** That means every row, both
128 themes, every corner, and open and collapsed states, in zoomed crops at 1× and
129 2×. Check clip regions and stacking order; one border was drawn over a select
130 menu. A hover fix checked on one row missed the fourth.
131- **Change only what was asked.** Overcorrecting a neighbouring radius or
132 margin buys another round.
133
134## 6. Colour and theme
135
136- **Derive colours from a hue**, then tune saturation and lightness separately
137 for light and dark. Never mix toward the background: "the color uses the hue
138 but an either mostly dark or light color, and then tune the
139 saturation/lightness".
140- **A hover fade stays inside one colour family**, never grey into an accent.
141- **In dark mode, borders are dark greys, not the background colour.** Lift any
142 stored colour too dark to read. Judge light mode on its own as well; its
143 loading skeletons were "a bit harsh".
144- **Identity colours stay stable.** Key them to a hash of the id, never to rank
145 or size, so a rename doesn't recolour anything.
146- **Keep status colours distinct from series colours.** Check named swatches
147 against reference values: "silver" is not silver.
148
149## 7. Icons
150
151- **Match the owner's icon style.** If it is pictorial, as Clover's
152 half-skeuomorphism is, each icon is a small coloured picture of its object,
153 and a generic web icon set reads as cheap: "this looks like lucide … we can
154 do better".
155- **Judge icons at their shipped size** (16 px) against every accent colour, in
156 light and dark. Centre them optically. Structural parts like arrows and page
157 edges take the label colour at an opacity, so they read on any highlight.
158- **Brand marks are the brand's own**, shown untouched: "no half skeomorphism on
159 these because theyre brands".
160- **Ask before deleting icon art in a cleanup pass.** Clover keeps every icon,
161 used or not.
162- **Comparison sheets show differences at shipped size.** Otherwise the owner
163 asks, as Clover did, "A and C in your image look the same?"
164
165## 8. Density
166
167- **Size controls for the command count you'll have later**, not today's: "i'll
168 want to add a lot more of them later so they should be smaller with tighter
169 spacing."
170- **Menus use the platform's density**: "context menus gotta not have crazy
171 padding".
172- **Separate with spacing, not rules**: "this ui is kind of busy with
173 horizontal lines".
174- **Never show the same state twice**: "like girl we know what the storage
175 meter means".
176- **Write case into the source** and keep canonical names. Clover also bans
177 `text-transform` and strings of facts joined by dots; check the owner's
178 preference.
179- **Centre a tooltip on its control.** Its words follow `ui-copy`. A chart's
180 hover readout lets the pointer pass through, unless something in it has to be
181 clicked.
182
183## 9. When it "looks off"
184
185- **The owner can't name the problem:** send a labelled A/B/C sheet of the
186 likely causes. Snowbound's first shell went from "there's something off
187 looking about the ui screenshot but i cant place it exactly" to a batch of
188 precise fixes in one round.
189- **Open-ended taste:** ship switchable variants (an env var or a mock control)
190 with a comparison matrix, then tune from the owner's notes: "i know theres a
191 version of this that can look good, can you iterate on it a bunch?"
192- **Report numeric values** (opacity, radius, gradient stops), and keep the
193 previous ones so the owner can steer by deltas: "maybe put it halfway between
194 here and last turn".
195
196## 10. The owner's taste
197
198Taste is the owner's. Look for it where they keep it: AGENTS.md or CLAUDE.md,
199memory, the brief, or the apps they've already made. If you find nothing, ask
200once, or offer a direction with alternatives.
201
202For example, this is Clover's, gathered from her three apps:
203
204- **Half-skeuomorphism:** modern shapes, concentric corners, soft shadows,
205 tasteful gradients and outlines, and a real dark mode. "my so called "half
206 skeomorphism" by using modern shapings but gradients and outlines
207 tastefully."
208- **Depth over flat**: "i think the answer is a middle ground where we add some
209 depth".
210- **Responsiveness like File Pilot**: "it's ui is particularly really enjoyable
211 to use, particularly it's animations and responsiveness."
212- **Dense but calm lists** like VS Code's, "maybe not AS tight".
213- **Dashboards, from the snow globe:**
214 - lists run edge to edge under a fixed, collapsible header;
215 - headline numbers are small pills inline with the title;
216 - the live value sits inside its chart;
217 - no scoreboard tiles, floating cards or padding walls.
218- **Screenshots that sell**: "i'm just trying to think of all the things we can
219 do to maximize aura of a screenshot."