Why prompt-level advice underperforms
Prompt advice is not wrong, it is just short-lived. Told to use a warm palette, the agent will. Six screens later it has the adjective and not the values, so it approximates — and the approximations diverge. That is the mechanism behind why AI-built sites converge, and it is why the fixes below are all written as values rather than as instructions.
Rule of thumb while reading: if a change can be expressed as a number or a hex, put it in tokens. If it can only be expressed as an adjective, you have not finished making the decision.
1. Tint the neutrals toward your accent
This is the highest-return change and almost nobody makes it. Default themes pair a pure grey ramp with an unrelated accent, and the two never look like they belong to the same product. Designed systems bias the neutrals a few degrees toward the accent hue, at very low chroma.
/* Default: pure neutral, unrelated to the accent */
--background: oklch(0.145 0 0);
--muted: oklch(0.269 0 0);
--primary: oklch(0.55 0.19 45);
/* Tinted: same lightness, a trace of the accent hue */
--background: oklch(0.145 0.008 45);
--muted: oklch(0.269 0.012 45);
--primary: oklch(0.55 0.19 45);Keep the chroma genuinely low. Above roughly 0.02 the greys start reading as coloured rather than as neutral, and you have traded one obvious look for another.
2. Add a second type family
A single family doing headings, body, labels and numbers is the fastest way to spot generated UI. The fix is a pairing with real contrast — not two similar grotesques, which reads as an accident rather than a decision.
- Serif display + grotesque body. The most reliable contrast, and hard to get wrong.
- Grotesque display + humanist body. Quieter; works for dense product UI where a serif would feel decorative.
- One superfamily, two extremes. A condensed heavy against a regular. Safe when the brand needs to stay tight.
- A mono for data. Not decoration: tabular figures stop numbers jittering between rows.
Whatever you pick, write the exact families, weights and letter-spacing into tokens. *Pair a serif with a sans* is an adjective, and adjectives decay.
3. Scale radius to element size
One --radius applied everywhere is the third tell. Radius reads *relative to the element it sits on*, so the same 8px makes a small button look blobby and a large card look barely rounded.
/* One value, applied to everything */
--radius: 0.5rem;
/* Scaled to element size */
--radius-sm: 0.25rem; /* inputs, chips, badges */
--radius-md: 0.5rem; /* buttons, small controls */
--radius-lg: 0.875rem; /* cards, panels */
--radius-xl: 1.25rem; /* modals, sheets, hero surfaces */4. Break the uniform grid
Three equal cards says every item has equal weight. That is almost never true, and a layout that says it anyway carries no information about the product.
The change is editorial rather than technical: decide which item actually matters most and let the layout say so. One wide card above two narrow ones. A two-thirds/one-third split. A single feature with real supporting imagery instead of three icons. The grid is fine when the items genuinely are peers — the failure is using it by default.
Consistency check · No type scale
The same plan card, built two ways in Mauve Broadcast.
Pricing
Everything a small team needs to ship a branded UI.
Pricing
Everything a small team needs to ship a branded UI.
5. Give elevation a model
Generated UI usually has one shadow, applied to anything that should look raised. Real elevation systems have two or three levels that agree about where the light is, and they use border and background shifts as well as shadow — often instead of it.
/* One shadow doing every job */
--shadow: 0 1px 3px rgb(0 0 0 / 0.1);
/* Levels that agree about a light source */
--elevation-0: none; /* flush: use a border */
--elevation-1: 0 1px 2px rgb(0 0 0 / 0.06); /* cards */
--elevation-2: 0 4px 12px rgb(0 0 0 / 0.08); /* dropdowns */
--elevation-3: 0 16px 40px rgb(0 0 0 / 0.12); /* modals */6. Write it where the agent will re-read it
The five changes above are worth little if they live in a conversation. Put them in a DESIGN.md plus token files in the repo, and the agent resolves the same values on every run — including in a session opened next month.
- 1
Install the design contract
Writes DESIGN.md and the token files into the project.
npx --yes identityforge@latest install --client claude-code - 2
Apply a kit that already makes these choices
Tinted neutrals, a real pairing, scaled radius and an elevation model, already decided.
identityforge apply terrain-vivant - 3
Reference it from your agent file
One line in AGENTS.md so the contract is found rather than stumbled upon.
Never hardcode theme colors. Use the semantic tokens in DESIGN.md. - 4
Check it held
Open a new session, build an unrelated screen, and compare its primary and radius against the first. Identical means the values are being read, not remembered.
What still looks generated after all six
Every guide on this SERP ends at the fix list. That is the wrong place to stop, because two tells survive all six changes and they are the ones people notice without being able to name. Both are outside the token file as it is usually written, which is exactly why they persist.
Everything moves the same way
Generated interfaces have one animation: a short fade with a small upward translate, applied to every element that appears. It is on the hero, the cards, the modal and the toast. Nothing in the motion tells you what kind of thing just happened, so the whole product feels like one continuous slideshow.
Motion carries meaning when duration and easing vary by *what the element is doing*, not by where it sits. The distinction to encode is between things that respond to you and things that arrive on their own:
| What it is | Duration | Easing | |
|---|---|---|---|
| Responding to a click or hover | A control acknowledging you | 100–150ms | ease-out — fast start, it should feel already done |
| Something entering | A menu, popover or panel | 200–250ms | ease-out, with the transform doing most of the work |
| Something leaving | The same element dismissed | 120–180ms | ease-in — exits should be faster than entrances |
The asymmetry in that last row is the part almost nothing generated gets right: exits run at the same speed as entrances, which makes dismissing anything feel sticky. Write the three durations as tokens and the agent stops picking one number for everything.
/* One duration, one curve, applied to everything */
--transition: 200ms ease;
/* Motion that says what kind of event it was */
--motion-response: 120ms cubic-bezier(0, 0, 0.2, 1);
--motion-enter: 220ms cubic-bezier(0, 0, 0.2, 1);
--motion-exit: 160ms cubic-bezier(0.4, 0, 1, 1);
@media (prefers-reduced-motion: reduce) {
--motion-response: 1ms;
--motion-enter: 1ms;
--motion-exit: 1ms;
}Every product uses the same icons
The second survivor is iconography. A default install gives you one open-source set at one stroke weight, and because that set ships with the component library, the same twenty glyphs appear across thousands of products. You can change every colour and every typeface and still be recognisable from the icons alone.
Replacing the set is usually not worth it. Constraining it is:
- Fix the stroke weight to your type weight. A 1.5px stroke against a light body face looks borrowed; against a medium one it looks matched. Pick one value and put it in tokens.
- Fix the size to the text it sits beside, so an icon in a button and an icon in a table row are not the same pixel size by accident.
- Ban the decorative ones. Prohibit icons that stand in for a mood rather than an action — the sparkles, rockets and lightning bolts are the single strongest generated-UI signal that survives a full retheme.
- Allow one exception and name it. A single custom glyph, usually the brand mark, used in one consistent place. One deliberate departure reads as a decision; several read as an accident.
The sparkle test
If a screen contains a sparkle icon, a gradient from violet to blue, and the word "seamlessly", none of the six token changes above will save it. Those three are content and iconography decisions, and they have to be prohibited in writing because no palette can override them.
What the files people actually write get wrong
Writing the six changes down is the easy half. We sampled 299 public DESIGN.md files from GitHub to see what people actually commit, and the failure pattern is consistent enough to be worth planning around.
| Share of files | |
|---|---|
| Omit dark mode entirely | 69% |
| Use no semantic colour role names | 86% |
| State no prohibitions | 76% |
| State no motifs or principles | 57% |
| Contain no concrete size value at all | 44% |
| Use a vague adjective where a value belongs | 54% |
| Specify a radius scale | 21% |
Read that against the six changes above and the overlap is exact. Radius scaling is specified by one file in five. Prohibitions — the cheapest thing to write and the most reliably followed — are absent from three quarters. And the most common words standing in for a decision were *clean* (39% of files), *modern* (36%) and *professional* (22%).
Writing it down is not the same as deciding it
Over half of real files contain at least one adjective where a value belongs. An agent handed 'clean and modern' has to resolve it, and it resolves it differently every time. If a line could produce two different screens for two competent readers, the decision has not been made yet.
The practical consequence for the six changes: write each one as a number or a hex, and check your own file for the three words above before you commit it.
All six, already decided
Each Identity Forge kit ships tinted neutrals, a real font pairing, a scaled radius set and an elevation model — with the DESIGN.md that makes them stick across sessions. Free kits need no account.
Which single change gives the most improvement?
Tinting the neutrals. It requires no layout work, no new fonts and no redesign — you are adjusting chroma on values you already have, and it changes how the entire page reads.
Will a second font hurt performance?
Marginally, and it is manageable. Two families at two weights each is roughly 60–120KB with woff2 and subsetting, loaded once. Variable fonts cut it further. That is a fair trade for the largest visual tell on the page.
Do I need OKLCH for the tinted neutrals?
No, but it makes it much easier. OKLCH separates lightness from chroma, so you can add a trace of hue without changing perceived brightness. In HSL the same edit shifts lightness too, and you end up re-tuning the ramp.
Is this just theming?
Partly, and that is the point — most of what reads as 'generic' lives in values that a theme layer already owns. The parts that are not theming are the grid decision and the elevation model, which are structural.
How do I stop the agent overriding these?
State the constraint as a prohibition rather than a preference. 'Never hardcode theme colors; use the semantic tokens' is followed far more reliably than 'prefer semantic tokens'.