Getting Started

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
# 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 values

Step 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

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)

tokens/semantic/colors.json
{
  "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

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)

tokens/semantic/scale.json (px overrides)
{
  "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

tokens/components/button.json (radius override)
{
  "button": {
    "radius": {
      "$type": "dimension",
      "$value": "var(--component-radius-md)",
      "$description": "Verdant: rounder button"
    }
  }
}

tokens/components/badge.json

tokens/components/badge.json (radius override)
{
  "badge": {
    "radius": {
      "$type": "dimension",
      "$value": "var(--radius-full)",
      "$description": "Verdant: pill badges — botanical feel"
    }
  }
}

tokens/output/themes/verdant.css

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

lib/themes.ts (verdant entry)
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:

styles/globals.css
@import "../tokens/output/themes/verdant.css";   ← add this line

Update lib/themes.ts

Add the verdant entry to COLOR_THEMES.


Step 5 — Build and preview

npm run tokens:build
npm run dev

Open 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

SymptomCauseFix
Dark mode has no color changesAI forgot $extensions.mode.darkAdd dark values to each token in colors.json
Buttons ignore theme radiusAI used hard-coded 0.5rem instead of var(--radius-sm)Change to var(--component-radius-sm)
Theme CSS not appliedImport missing from globals.cssAdd @import line
Padding too tight after buildAI put px overrides in theme CSS but not in scale.jsonMerge into scale.json so the build propagates them
OKLCH chroma > 0.4AI hallucinated vivid valuesClamp to 0.35 max for primary colors
--color-primary utility missingTheme palette not mapped in @theme inlineRun npm run tokens:build again — the build generates mappings automatically
Swatch in ThemeSwitcher is wrong colorlib/themes.ts primary value doesn't match tokens.cssUpdate 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.

We use cookies

We use cookies to ensure you get the best experience on our website. For more information on how we use cookies, please see our cookie policy.

By clicking Accept, you agree to our use of cookies.
Learn more.