# Why AI-Generated UI Looks AI-Generated: The Design Token Solution

> Discover why AI-generated UIs look inconsistent and how design tokens can create a cohesive visual language across your app.
> By Dave · 2026-07-18
> Source: https://otf-kit.dev/blog/design-tokens-stop-ai-slop

## The smell test

You asked your coding agent for a settings page on a Tuesday. It shipped in ninety seconds. Six months later, you can't tell that page was written by the same agent that wrote your billing page in March. Different blues. Different paddings. Different border radii. Different fonts you never asked for. Two screens, one product, no shared language.

Designers call this **drift**. In a hand-coded codebase it happens over quarters. In an agent-driven codebase it happens between prompts — because every agent reply is a fresh conversation with whatever it found in training, and training data is full of `bg-[#3b82f6]` soup.

Hex literals are how you tell a model *nothing* about your system. They are the absence of a system. That absence is what makes AI-generated UI look like AI-generated UI.

## What "ad-hoc styling" actually means in a generated file

Three properties show up together in any screen an agent emits without a contract:

```tsx
// SettingsPage.tsx — generated, three weeks ago
<div className="min-h-screen bg-[#fafafa]">
  <div className="max-w-[640px] mx-auto p-6">
    <h1 className="text-[22px] font-semibold text-[#111827] mb-4">
      Settings
    </h1>
    <button className="bg-[#3b82f6] text-white rounded-[6px] px-4 py-2 text-sm">
      Save changes
    </button>
  </div>
</div>
```

```tsx
// BillingPage.tsx — generated last Tuesday
<div className="min-h-screen bg-gray-50">
  <div className="max-w-2xl mx-auto p-8">
    <h1 className="text-2xl font-bold text-gray-900 mb-6">
      Billing
    </h1>
    <button className="bg-blue-600 text-white rounded-md px-4 py-2 text-sm">
      Update card
    </button>
  </div>
</div>
```

Both pages render. Both look fine. **But they are the same product built from two different design systems.** `#fafafa` against `gray-50`. `#111827` against `gray-900`. `#3b82f6` against `blue-600`. `text-[22px]` against `text-2xl`. `rounded-[6px]` against `rounded-md`. The list of disagreements is longer than the list of agreements.

These are not bugs. They are evidence that no contract existed between the agent and your app. Each prompt was a fresh roll of the dice, and the dice landed the way most tutorials land — same three blues, same five grays, same `rounded-md`.



![every prompt without a token contract is the agent inventing a new design system from scra](https://cdn.otf-kit.dev/blog/design-tokens-stop-ai-slop/inline-1.png)



## Why the soup is the default

The training set is dominated by CodePens, Medium tutorials, and one-off StackOverflow answers. Every one of those uses arbitrary values — because that is what tutorials do. `#3b82f6` is more copyable than `--color-accent-500`. The model learned to write code that compiles *visually*, not code that *fits a system* — because system-fit is rarely visible in the training distribution.

When you hand an agent a repo without an explicit token layer, every screen it writes is its own self-contained demo. The agent does not *know* your accent color — it has to invent one, and it invents one it has seen ten thousand times. That is why everything AI-spawned settles into the same three blues, the same five grays, the same `rounded-md`. Pull the Figma files for any five Lovable or Bolt exports and the same palette is on every page, in the same order, with the same `tracking-tight`. Not because their users asked for it. Because the model does not have any other color to offer.

## The fix is one file the agent reads

Tokens are not a styling library. They are a **contract**: a named list of choices the agent can address by name instead of inventing. One file. Three sections. Names that survive renames.

```ts
// tokens.ts — the entire design system, in plain TypeScript
export const color = {
  bg:        '#ffffff',   // page background
  surface:   '#f9fafb',   // raised panels
  border:    '#e5e7eb',
  fg:        '#111827',   // body text
  muted:     '#6b7280',   // secondary text
  accent:    '#3b82f6',   // primary action
  accentFg:  '#ffffff',
  danger:    '#ef4444',
  success:   '#10b981',
} as const

export const space = {
  1: '4px',
  2: '8px',
  3: '12px',
  4: '16px',
  6: '24px',
  8: '32px',
  12: '48px',
} as const

export const radius = {
  sm: '4px',
  md: '8px',
  lg: '12px',
} as const
```

Now the agent has names to ask for. "Use the accent button" is a target. "Use a hex that looks like accent" is not. Three scales is enough to start — color, space, radius. Add type, shadow, and motion when the screen count climbs.

[[DIAGRAM: agent → reads CLAUDE.md → reads tokens.ts → writes a page referencing color.accent and space[4] → a rebrand is then a single edit to tokens.ts that flows to every page that imported it]]

## A real example, after the contract

The two pages from above, with the token layer in place:

```tsx
// SettingsPage.tsx
import { color, space, radius } from './tokens'

<div style={{
  minHeight: '100vh',
  background: color.bg,
  padding: space[6],
}}>
  <h1 style={{ color: color.fg, marginBottom: space[4] }}>
    Settings
  </h1>
  <button style={{
    background: color.accent,
    color: color.accentFg,
    borderRadius: radius.md,
    padding: `${space[2]} ${space[4]}`,
  }}>
    Save changes
  </button>
</div>
```

```tsx
// BillingPage.tsx
import { color, space, radius } from './tokens'

<div style={{
  minHeight: '100vh',
  background: color.bg,
  padding: space[6],
}}>
  <h1 style={{ color: color.fg, marginBottom: space[4] }}>
    Billing
  </h1>
  <button style={{
    background: color.accent,
    color: color.accentFg,
    borderRadius: radius.md,
    padding: `${space[2]} ${space[4]}`,
  }}>
    Update card
  </button>
</div>
```

Same structure. Both buttons resolve to `color.accent`. Both headings resolve to `color.fg`. Both have `radius.md`. **Change `color.accent` in one file and both pages move together on every platform that imports the same module.** That is not true of `#3b82f6` and `blue-600` — there is no file you can edit that reaches both, because they were never the same choice to begin with. Hex soup has no addressable surface.

## Tell the agent where the contract is

A token file that the agent has never read is a token file that does not exist for the agent. Put it in `CLAUDE.md` (and `.cursorrules`, and the equivalent for whichever agents you run):

```md
# Design contract
Read `tokens.ts` before writing any styled component.
Use the named exports (`color.accent`, `space[4]`, `radius.md`).
Never use hex literals or raw color names on this codebase.
If a value you need isn't in the file, add it there first.
```

That is it. Three sentences. The "if it isn't in the file, add it there first" line is the load-bearing clause — it turns the token file into a *growing* contract instead of a frozen one. Most designers ship a design system in three rounds: enough for v1, enough for v1.5, enough for the next agent to extend without asking.

## Drift is a contract problem, not a tooling problem

A real design system is not the 200 components in the library. It is the 200 components standing on a 30-line file. The 30-line file is what makes the components coherent — pull it out and the components become 200 individual opinions about what a button looks like, which is exactly where most agent-generated codebases live today.

You can swap the model on Tuesday and the next agent will resume the work cleanly, because it will read `tokens.ts` before it touches a button. The tokens outlive the agent. The slow files outlive the fast ones. That is the test of a real design contract: it should be the slowest-moving file in your repo, and every other styled file should treat it as ground truth.

## How to actually do this without rewriting everything

If you already have a soup, do not rewrite. Migrate in three steps:

```bash
# Step 1 — see what is actually drifting
rg -o '#[0-9a-f]{6}' src/ | sort | uniq -c | sort -rn | head -20
```

The top of that output is your accidental palette. Take the top three hexes, give them real names (`color.bg`, `color.fg`, `color.accent`), drop them in `tokens.ts`, and replace the literals across the codebase — a single search across template strings, or a `sed` if the values live in raw CSS. Then do the same for `space` (the four `p-*` / `padding-*` values actually used) and `radius` (the two `rounded-*` values actually used). Stop there. Add tokens on demand when the next screen asks for a value you have not named yet.

The first time an agent generates a new screen *after* the contract is in place, it will reach for `color.accent` because the file told it to, not because it read six months of commit history. That first agent output is the moment the codebase stops drifting.

---

Both pages above are running on the same token layer inside the SaaS Dashboard kit — a one-line diff reskins web and native together, because the same `color.accent` resolves on iOS, Android, and web from the same module. The token set ships as `@otfdashkit/tokens` on npm, MIT-licensed; the full SaaS Dashboard kit (auth, billing, DB, Stripe wired) is paid but you own the output and the token layer is the part you will not have to rewrite in two years.