| 1 | --- |
| 2 | name: ui-craft |
| 3 | description: 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 | |
| 8 | The examples come from Clover's apps: Snowbound (a OneNote 2010 remake), Clover |
| 9 | Chat (a chat multiplexer) and the snow globe dashboard (a home-server console). |
| 10 | The quotes are hers. Visual problems were the most common correction across all |
| 11 | three. Most extra rounds came from two habits: guessing a geometry, timing or |
| 12 | platform behaviour that could have been measured, and fixing one instance |
| 13 | without 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 | |
| 49 | Let the platform own what people know by hand: file pickers, alerts, the caret |
| 50 | and selection colours, editing chords, window frames and traffic lights, |
| 51 | scrollbars, 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 | |
| 198 | Taste is the owner's. Look for it where they keep it: AGENTS.md or CLAUDE.md, |
| 199 | memory, the brief, or the apps they've already made. If you find nothing, ask |
| 200 | once, or offer a direction with alternatives. |
| 201 | |
| 202 | For 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." |