shadcn/ui Theme Generator (OKLCH)

shadcn/ui components read their colours from CSS variables such as --primary, --muted and --border, written in OKLCH. Choose a hue, chroma, lightness, grey tint and corner radius here, check the live light and dark preview with contrast ratios, then paste the generated :root and .dark blocks into your global stylesheet.

Variable pairs every shadcn/ui theme defines
SurfaceText on itUsed by
--background--foregroundPage body
--card / --popover--card-foreground / --popover-foregroundCards, menus, dialogs
--primary--primary-foregroundDefault buttons, active states
--secondary / --muted--secondary-foreground / --muted-foregroundQuiet buttons, hints, captions
--accent--accent-foregroundHovered menu items

Updated

The theming docs define about thirty variables, and editing them by hand is slow because a change to the primary colour also changes which text colour stays readable on it. This generator derives the whole set from a few choices, picks a readable foreground for each surface, and flags any pair that falls below common contrast targets.

Build your theme

Presets: Neutral, Blue, Violet, Green, Orange, Rose. Adjust hue, chroma, lightness, grey tint and radius, preview light and dark, and copy the CSS.

Example output: the Blue preset

This is exactly what the generator writes for the Blue preset. Paste it over the colour blocks in globals.css.

globals.css colour blocks

Light values sit in :root and dark values in .dark, both in OKLCH.

:root {
  --radius: 0.625rem;
  --background: oklch(1 0.001 258);
  --foreground: oklch(0.145 0.006 258);
  --card: oklch(1 0.001 258);
  --card-foreground: oklch(0.145 0.006 258);
  --popover: oklch(1 0.001 258);
  --popover-foreground: oklch(0.145 0.006 258);
  --primary: oklch(0.55 0.2 258);
  --primary-foreground: oklch(0.985 0.002 258);
  --secondary: oklch(0.97 0.006 258);
  --secondary-foreground: oklch(0.205 0.006 258);
  --muted: oklch(0.97 0.006 258);
  --muted-foreground: oklch(0.556 0.006 258);
  --accent: oklch(0.96 0.04 258);
  --accent-foreground: oklch(0.205 0.006 258);
  --destructive: oklch(0.577 0.23 27);
  --border: oklch(0.922 0.006 258);
  --input: oklch(0.922 0.006 258);
  --ring: oklch(0.55 0.2 258);
  --chart-1: oklch(0.65 0.2 258);
  --chart-2: oklch(0.65 0.2 320);
  --chart-3: oklch(0.65 0.2 22);
  --chart-4: oklch(0.65 0.2 84);
  --chart-5: oklch(0.65 0.2 146);
  --sidebar: oklch(0.985 0.003 258);
  --sidebar-foreground: oklch(0.145 0.006 258);
  --sidebar-primary: oklch(0.55 0.2 258);
  --sidebar-primary-foreground: oklch(0.985 0.002 258);
  --sidebar-accent: oklch(0.96 0.04 258);
  --sidebar-accent-foreground: oklch(0.205 0.006 258);
  --sidebar-border: oklch(0.922 0.006 258);
  --sidebar-ring: oklch(0.55 0.2 258);
}

.dark {
  --background: oklch(0.145 0.006 258);
  --foreground: oklch(0.985 0.002 258);
  --card: oklch(0.205 0.006 258);
  --card-foreground: oklch(0.985 0.002 258);
  --popover: oklch(0.205 0.006 258);
  --popover-foreground: oklch(0.985 0.002 258);
  --primary: oklch(0.65 0.2 258);
  --primary-foreground: oklch(0.205 0.006 258);
  --secondary: oklch(0.269 0.006 258);
  --secondary-foreground: oklch(0.985 0.002 258);
  --muted: oklch(0.269 0.006 258);
  --muted-foreground: oklch(0.708 0.006 258);
  --accent: oklch(0.3 0.06 258);
  --accent-foreground: oklch(0.985 0.002 258);
  --destructive: oklch(0.704 0.19 22);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.65 0.2 258);
  --chart-1: oklch(0.7 0.2 258);
  --chart-2: oklch(0.7 0.2 320);
  --chart-3: oklch(0.7 0.2 22);
  --chart-4: oklch(0.7 0.2 84);
  --chart-5: oklch(0.7 0.2 146);
  --sidebar: oklch(0.205 0.006 258);
  --sidebar-foreground: oklch(0.985 0.002 258);
  --sidebar-primary: oklch(0.65 0.2 258);
  --sidebar-primary-foreground: oklch(0.205 0.006 258);
  --sidebar-accent: oklch(0.3 0.06 258);
  --sidebar-accent-foreground: oklch(0.985 0.002 258);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.65 0.2 258);
}

Adding your own colour token

A warning colour usable as bg-warning

Define the pair in both modes, then expose it to Tailwind through @theme inline.

:root {
  --warning: oklch(0.82 0.15 80);
  --warning-foreground: oklch(0.3 0.06 55);
}
.dark {
  --warning: oklch(0.47 0.1 60);
  --warning-foreground: oklch(0.98 0.02 90);
}
@theme inline {
  --color-warning: var(--warning);
  --color-warning-foreground: var(--warning-foreground);
}

How the palette is derived

Your primary colour feeds the primary, ring and sidebar-primary variables. Greys for backgrounds, borders and muted text share one hue with a tiny chroma set by the tint slider, so the interface feels related to the brand without looking coloured.

Each foreground is chosen by measuring contrast against its surface and keeping whichever of a near-white or near-black reads better. Chart colours step around the hue wheel from your primary hue so series stay distinguishable.

Dark mode details

In the dark block, borders and inputs use white at low alpha instead of a solid grey, which lets them adapt to whatever surface they sit on. Cards are lifted slightly above the page background so layers stay visible.

Test focus rings in dark mode in particular. A ring that is clear on white can vanish on a deep navy page.

How it works

  1. Start from a preset, or set the primary hue, chroma and lightness with the sliders. The swatch shows the colour and warns if it sits outside the sRGB range.
  2. Raise the grey tint to give backgrounds, borders and muted text a hint of the brand hue. Zero keeps them pure neutral.
  3. Switch the preview between light and dark and read the contrast table for text on each surface.
  4. Copy the CSS and replace the matching :root and .dark blocks in your globals.css. Keep the existing @theme inline block, which maps these variables to Tailwind utilities.

Tips

Frequently asked questions

Why OKLCH instead of HSL?

OKLCH lightness tracks how bright a colour looks, so two colours with equal L feel equally light even when their hues differ. That makes it easier to build matching light and dark sets. The current shadcn/ui theming docs use OKLCH for every colour.

What does each OKLCH number mean?

L is lightness from 0 (black) to 1 (white), C is chroma, how vivid the colour is, starting at 0 for grey, and H is the hue angle in degrees. An optional alpha follows a slash, as in oklch(1 0 0 / 10%).

What does --radius change?

Components round their corners from this one value. The theming docs set it to 0.625rem by default, and the @theme inline block derives smaller and larger radius steps from it.

Will this work with older projects?

Projects on older templates may store colours in a different format and map them differently in the Tailwind config. Compare your globals.css with the variables here before replacing anything.

Are the contrast ratios exact?

They use the WCAG 2 formula after converting each OKLCH colour to sRGB, clipping any out-of-range value. Colours outside sRGB will display slightly differently on wide-gamut screens.

Related: shadcn/ui examples, shadcn form and table, Tailwind colour palette generator, Colour contrast checker, Dark mode for websites, CSS variables guide.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Free Tools

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap