AI Theme Generation
Complete walkthrough — write a design.md for a brand called "Verdant", run the AI prompt, review every generated file, run the build, and see the result in the ThemeSwitcher.
The example brand: Verdant
We'll build a complete theme for a fictional product called Verdant — a sustainability analytics platform. The brand personality is calm, grounded, and data-forward.
Brand brief:
- Colors: forest green primary, warm off-white backgrounds, earthy neutrals
- Shape: generously rounded (approx. 0.875rem base radius)
- Density: default (same heights as shadcn, slightly wider padding)
- Audience: sustainability managers and ESG analysts — desktop-first, data-heavy
Step 1 — Write design.md
Create design.md at the project root:
# design.md — Verdant
version: 1.0
updated: 2025-06-03
## 1. Brand
Name: Verdant
Tagline: Data for a liveable planet
Personality: calm, grounded, data-forward, trustworthy
Target audience: sustainability managers and ESG analysts, desktop-first
Design principles:
1. Nature-derived palette — greens feel honest, not trendy
2. Calm defaults — no strong saturations, no aggressive contrasts
3. Readable at a glance — enough whitespace for scannability
## 2. Colors
Primary hue: 149 (forest green)
Neutral hue: 120 (slight warm-green tint on greys)
Palette (tokens/base/palette.json):
| Name | Value |
|----------------|-------------------------------|
| green-50 | oklch(0.97 0.02 149) |
| green-100 | oklch(0.93 0.05 149) |
| green-200 | oklch(0.86 0.09 149) |
| green-300 | oklch(0.76 0.13 149) |
| green-400 | oklch(0.66 0.17 149) |
| green-500 | oklch(0.58 0.19 149) |
| green-600 | oklch(0.50 0.18 149) |
| green-700 | oklch(0.40 0.14 149) |
| green-800 | oklch(0.28 0.09 149) |
| green-900 | oklch(0.17 0.05 149) |
| warm-50 | oklch(0.985 0.006 120) |
| warm-100 | oklch(0.96 0.009 120) |
| warm-200 | oklch(0.91 0.010 120) |
| warm-300 | oklch(0.82 0.010 120) |
| warm-400 | oklch(0.68 0.010 120) |
| warm-500 | oklch(0.53 0.010 120) |
| warm-600 | oklch(0.40 0.009 120) |
| warm-700 | oklch(0.28 0.008 120) |
| warm-800 | oklch(0.19 0.007 120) |
| warm-900 | oklch(0.12 0.006 120) |
Semantic colors (tokens/semantic/colors.json):
- background: oklch(0.985 0.006 120) → oklch(0.13 0.007 120)
- foreground: oklch(0.17 0.05 149) → oklch(0.97 0.02 149)
- card: oklch(1 0 0) → oklch(0.19 0.007 120)
- card-foreground: same as foreground
- popover: same as card
- popover-foreground: same as foreground
- sidebar: oklch(0.96 0.009 120) → oklch(0.17 0.007 120)
- sidebar-foreground: same as foreground
- primary: oklch(0.50 0.18 149) → oklch(0.66 0.17 149)
- primary-foreground: oklch(0.985 0 0) → oklch(0.985 0 0)
- secondary: oklch(0.93 0.05 149) → oklch(0.22 0.06 149)
- secondary-foreground: oklch(0.17 0.05 149) → oklch(0.97 0.02 149)
- muted: oklch(0.96 0.009 120) → oklch(0.20 0.007 120)
- muted-foreground: oklch(0.40 0.009 120) → oklch(0.63 0.010 120)
- accent: oklch(0.93 0.05 149) → oklch(0.22 0.06 149)
- accent-foreground: same as foreground
- destructive: oklch(0.577 0.245 27) → oklch(0.396 0.141 26)
- destructive-foreground: oklch(0.985 0 0) → oklch(0.985 0 0)
- info: oklch(0.55 0.20 220) → oklch(0.65 0.18 220)
- info-foreground: oklch(0.985 0 0) → oklch(0.985 0 0)
- success: oklch(0.50 0.18 149) → oklch(0.66 0.17 149)
- success-foreground: oklch(0.985 0 0) → oklch(0.985 0 0)
- warning: oklch(0.75 0.18 70) → oklch(0.65 0.17 58)
- warning-foreground: oklch(0.17 0 0) → oklch(0.985 0 0)
- border: oklch(0.89 0.010 120) → oklch(1 0 0 / 10%)
- input: oklch(0.89 0.010 120) → oklch(1 0 0 / 15%)
- ring: oklch(0.50 0.18 149) → oklch(0.66 0.17 149)
- sidebar-primary: same as primary
- sidebar-primary-foreground: same as primary-foreground
- sidebar-accent: same as accent
- sidebar-accent-foreground: same as accent-foreground
- sidebar-border: same as border
- sidebar-ring: same as ring
- chart-1: oklch(0.50 0.18 149) → oklch(0.66 0.17 149)
- chart-2: oklch(0.55 0.20 220) → oklch(0.70 0.17 162)
- chart-3: oklch(0.38 0.07 227) → oklch(0.77 0.19 70)
- chart-4: oklch(0.83 0.19 84) → oklch(0.63 0.27 304)
- chart-5: oklch(0.77 0.19 70) → oklch(0.65 0.25 16)
## 3. Shape
Base radius: 0.875rem (rounded, approx. between 0.75 and 1rem)
Rationale: Verdant is approachable and natural. Rounder corners soften the
density of data tables without going full-pill.
## 4. Spacing & component scale
Density: default (keep shadcn heights)
Padding: slightly wider than default
component.px.md: calc(var(--spacing) * 5) ← was 4
component.px.lg: calc(var(--spacing) * 7) ← was 6
## 5. Typography
Keep defaults — Inter Variable already loaded.
## 6. Component overrides
button:
radius: var(--component-radius-md) ← rounder default
badge:
radius: var(--radius-full) ← pill badges feel botanical
card:
radius: var(--radius-xl) ← extra-open cards for data panels
## 7. Motion
Keep defaults.
## 8. Themes
Named theme: verdant (used as data-theme="verdant")
This IS the default theme — the semantic colors above ARE the verdant identity.
Register it in lib/themes.ts as an additional named theme so users can switch to it.
## 9. Files for the AI to generate
1. tokens/base/palette.json — green-* and warm-* palette
2. tokens/semantic/colors.json — all semantic tokens with dark values
3. tokens/semantic/radii.json — base: 0.875rem
4. tokens/semantic/scale.json — px.md and px.lg overrides only
5. tokens/components/button.json — radius override
6. tokens/components/badge.json — radius override
7. tokens/components/card.json — radius override (if file exists)
8. tokens/output/themes/verdant.css — [data-theme='verdant'] overrides
9. lib/themes.ts — add verdant entry to COLOR_THEMES
DO NOT generate: tokens/output/tokens.css, tokens/output/tokens.theme.css,
any registry/** files, styles/globals.css
## 10. Validation checklist
- [ ] All OKLCH L values between 0.0 and 1.0
- [ ] All OKLCH C values between 0.0 and 0.40
- [ ] Dark values in $extensions.mode.dark only
- [ ] No hard-coded px in Tier 2/3 — use calc(var(--spacing) * N)
- [ ] button.radius = var(--component-radius-md)
- [ ] badge.radius = var(--radius-full)
- [ ] verdant theme CSS uses [data-theme='verdant'] selector
- [ ] lib/themes.ts entry has correct OKLCH swatch valuesStep 2 — Run the AI prompt
Open your AI tool and send the prompt template from the design.md reference with your design.md pasted in.
Step 3 — Review the generated files
The AI should return these files. Below is the exact expected output for the Verdant brand.
tokens/base/palette.json
{
"green-50": { "$type": "color", "$value": "oklch(0.97 0.02 149)" },
"green-100": { "$type": "color", "$value": "oklch(0.93 0.05 149)" },
"green-200": { "$type": "color", "$value": "oklch(0.86 0.09 149)" },
"green-300": { "$type": "color", "$value": "oklch(0.76 0.13 149)" },
"green-400": { "$type": "color", "$value": "oklch(0.66 0.17 149)" },
"green-500": { "$type": "color", "$value": "oklch(0.58 0.19 149)" },
"green-600": { "$type": "color", "$value": "oklch(0.50 0.18 149)" },
"green-700": { "$type": "color", "$value": "oklch(0.40 0.14 149)" },
"green-800": { "$type": "color", "$value": "oklch(0.28 0.09 149)" },
"green-900": { "$type": "color", "$value": "oklch(0.17 0.05 149)" },
"warm-50": { "$type": "color", "$value": "oklch(0.985 0.006 120)" },
"warm-100": { "$type": "color", "$value": "oklch(0.96 0.009 120)" },
"warm-200": { "$type": "color", "$value": "oklch(0.91 0.010 120)" },
"warm-300": { "$type": "color", "$value": "oklch(0.82 0.010 120)" },
"warm-400": { "$type": "color", "$value": "oklch(0.68 0.010 120)" },
"warm-500": { "$type": "color", "$value": "oklch(0.53 0.010 120)" },
"warm-600": { "$type": "color", "$value": "oklch(0.40 0.009 120)" },
"warm-700": { "$type": "color", "$value": "oklch(0.28 0.008 120)" },
"warm-800": { "$type": "color", "$value": "oklch(0.19 0.007 120)" },
"warm-900": { "$type": "color", "$value": "oklch(0.12 0.006 120)" }
}tokens/semantic/colors.json (excerpt)
{
"background": {
"$type": "color",
"$value": "oklch(0.985 0.006 120)",
"$extensions": { "mode": { "dark": "oklch(0.13 0.007 120)" } }
},
"foreground": {
"$type": "color",
"$value": "oklch(0.17 0.05 149)",
"$extensions": { "mode": { "dark": "oklch(0.97 0.02 149)" } }
},
"primary": {
"$type": "color",
"$value": "oklch(0.50 0.18 149)",
"$description": "Verdant green — forest",
"$extensions": { "mode": { "dark": "oklch(0.66 0.17 149)" } }
},
"primary-foreground": {
"$type": "color",
"$value": "oklch(0.985 0 0)",
"$extensions": { "mode": { "dark": "oklch(0.985 0 0)" } }
},
"ring": {
"$type": "color",
"$value": "oklch(0.50 0.18 149)",
"$extensions": { "mode": { "dark": "oklch(0.66 0.17 149)" } }
}
}tokens/semantic/radii.json
{
"radius": { "$type": "dimension", "$value": "0.875rem", "$description": "Verdant base — approachable, natural" },
"radius-xs": { "$type": "dimension", "$value": "0.125rem" },
"radius-sm": { "$type": "dimension", "$value": "calc(var(--radius) - 4px)" },
"radius-md": { "$type": "dimension", "$value": "calc(var(--radius) - 2px)" },
"radius-lg": { "$type": "dimension", "$value": "var(--radius)" },
"radius-xl": { "$type": "dimension", "$value": "calc(var(--radius) + 4px)" },
"radius-2xl":{ "$type": "dimension", "$value": "calc(var(--radius) + 8px)" },
"radius-3xl":{ "$type": "dimension", "$value": "calc(var(--radius) + 12px)" },
"radius-4xl":{ "$type": "dimension", "$value": "calc(var(--radius) + 16px)" }
}tokens/semantic/scale.json (changed lines only)
{
"component": {
"px": {
"md": { "$type": "dimension", "$value": "calc(var(--spacing) * 5)", "$description": "Verdant: slightly wider than shadcn default" },
"lg": { "$type": "dimension", "$value": "calc(var(--spacing) * 7)", "$description": "Verdant: wider lg padding" }
}
}
}tokens/components/button.json
{
"button": {
"radius": {
"$type": "dimension",
"$value": "var(--component-radius-md)",
"$description": "Verdant: rounder button"
}
}
}tokens/components/badge.json
{
"badge": {
"radius": {
"$type": "dimension",
"$value": "var(--radius-full)",
"$description": "Verdant: pill badges — botanical feel"
}
}
}tokens/output/themes/verdant.css
/* ── Verdant Theme — calm · grounded · forest green ────────────────────────
Overrides only: primary, ring, sidebar colors, radius, and padding.
The full semantic color set is in tokens/semantic/colors.json.
────────────────────────────────────────────────────────────────────────── */
[data-theme='verdant'] {
--primary: oklch(0.50 0.18 149);
--primary-foreground: oklch(0.985 0 0);
--ring: oklch(0.50 0.18 149);
--sidebar-primary: oklch(0.50 0.18 149);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-ring: oklch(0.50 0.18 149);
--radius: 0.875rem;
--component-px-md: calc(var(--spacing) * 5);
--component-px-lg: calc(var(--spacing) * 7);
}
[data-theme='verdant'].dark {
--primary: oklch(0.66 0.17 149);
--primary-foreground: oklch(0.985 0 0);
--ring: oklch(0.66 0.17 149);
--sidebar-primary: oklch(0.66 0.17 149);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-ring: oklch(0.66 0.17 149);
}lib/themes.ts
export const COLOR_THEMES: ColorTheme[] = [
{
name: 'default',
label: 'Default',
description: 'Balanced · neutral zinc',
primary: 'oklch(0.205 0 0)',
primaryDark: 'oklch(0.985 0 0)',
},
// ... existing entries (rose, blue, green, orange) ...
{
name: 'verdant',
label: 'Verdant',
description: 'Calm · grounded · forest',
primary: 'oklch(0.50 0.18 149)',
primaryDark: 'oklch(0.66 0.17 149)',
},
]Step 4 — Merge the files
Apply palette and semantic tokens
Replace tokens/base/palette.json with the AI output.
For tokens/semantic/colors.json and tokens/semantic/radii.json, replace the whole file with the AI output — these files define the complete token set.
For tokens/semantic/scale.json, merge only the overridden keys — patch component.px.md and component.px.lg into the existing file without removing other keys.
Apply component overrides
For each component JSON, merge the override key into the existing file. For example, merge button.radius into the existing tokens/components/button.json which already has button.height.*, button.px.*, etc.
# Quick sanity check — confirm keys exist after merge
node -e "const f = require('./tokens/components/button.json'); console.log(f.button.radius)"Add the theme CSS file
Copy tokens/output/themes/verdant.css to the project. Then import it in styles/globals.css:
@import "../tokens/output/themes/verdant.css"; ← add this lineUpdate lib/themes.ts
Add the verdant entry to COLOR_THEMES.
Step 5 — Build and preview
npm run tokens:build
npm run devOpen the browser. The ThemeSwitcher now shows a new green swatch. Click it — every component on the page updates: backgrounds warm up, borders soften, buttons round out, badges become pills.
Step 6 — Validation
Run these checks to confirm the output is correct:
# 1. Confirm the CSS vars are in :root
node -e "
const fs = require('fs');
const css = fs.readFileSync('./tokens/output/tokens.css', 'utf-8');
['--primary','--background','--radius','--component-px-md'].forEach(v => {
console.log(v, css.includes(v) ? '✓' : '✗ MISSING');
});
"
# 2. Confirm dark vars are in .dark
node -e "
const css = require('fs').readFileSync('./tokens/output/tokens.css','utf-8');
const dark = css.slice(css.indexOf('.dark {'), css.indexOf('}', css.indexOf('.dark {')));
console.log('.dark block contains --primary:', dark.includes('--primary'));
"
# 3. Confirm theme CSS is correct
node -e "
const css = require('fs').readFileSync('./tokens/output/themes/verdant.css','utf-8');
console.log('[data-theme verdant] exists:', css.includes('[data-theme=\\'verdant\\']'));
console.log('[data-theme verdant].dark exists:', css.includes('[data-theme=\\'verdant\\'].dark'));
"Common AI mistakes to fix
| Symptom | Cause | Fix |
|---|---|---|
| Dark mode has no color changes | AI forgot $extensions.mode.dark | Add dark values to each token in colors.json |
| Buttons ignore theme radius | AI used hard-coded 0.5rem instead of var(--radius-sm) | Change to var(--component-radius-sm) |
| Theme CSS not applied | Import missing from globals.css | Add @import line |
| Padding too tight after build | AI put px overrides in theme CSS but not in scale.json | Merge into scale.json so the build propagates them |
| OKLCH chroma > 0.4 | AI hallucinated vivid values | Clamp to 0.35 max for primary colors |
--color-primary utility missing | Theme palette not mapped in @theme inline | Run npm run tokens:build again — the build generates mappings automatically |
| Swatch in ThemeSwitcher is wrong color | lib/themes.ts primary value doesn't match tokens.css | Update the swatch OKLCH to match the compiled value |
Iterating with AI
Once the base theme is generated, you can iterate with targeted follow-up prompts:
Refine a single color:
The current --muted-foreground at oklch(0.40 0.009 120) feels too dark in light mode.
Lighten it to approximately oklch(0.50 0.009 120) and update tokens/semantic/colors.json.
Add a high-contrast variant:
Create a [data-theme='verdant-hc'] block in tokens/output/themes/verdant.css
that passes WCAG AA. Use oklch(0.30 0.18 149) for --primary in light mode.
Generate a density variant:
Add [data-density='compact'] overrides to tokens/output/themes/verdant.css
that reduce all component heights by one spacing step and reduce px by 4px.
Export to Figma:
Based on tokens/output/figma.json, list all variables that changed in the
Verdant theme compared to the default. I'll update only those in Figma manually.