Tailwind v4 is the biggest break in the framework's history, and the part that catches the most teams off guard is colors. The JavaScript config that everybody was used to — tailwind.config.js with its theme.extend.colors object — is gone from the default setup. In its place is a CSS-first system built around the new @theme directive, CSS custom properties, and OKLCH as the default color space.
If you upgraded a v3 project and your custom palette stopped working, or you started a new v4 project and can't figure out where to put your brand colors, this guide is the missing manual. By the end you'll have a clean, type-safe Tailwind v4 color palette, a working dark mode, and a migration checklist that won't leave broken classes scattered across your codebase.
Generate a brand-safe palette in seconds, then export it directly as Tailwind v4 CSS variables — no signup, no markdown gymnastics.
Open the Color Palette Generator →Tailwind v4 moved configuration from JavaScript to CSS. The framework still ships every utility class you know (bg-blue-500, text-red-600, the lot), but the way you add or replace colors is now a CSS declaration, not a JS object. There are three concrete things to know:
@theme directive is the new home for design tokens. Anything you put there becomes both a CSS variable and a utility class.--color-blue-500), so you can read it from any CSS file or arbitrary value.The mental model is simple once you internalize it: your Tailwind theme is just a CSS file now. No more shuttling values between JS and CSS — the same declaration powers your utilities and your hand-written styles.
Here's the entire setup for a custom palette in a fresh v4 project. One file, one import, one @theme block:
/* app.css */
@import "tailwindcss";
@theme {
--color-brand-50: oklch(0.97 0.02 295);
--color-brand-100: oklch(0.93 0.05 295);
--color-brand-200: oklch(0.86 0.10 295);
--color-brand-300: oklch(0.77 0.15 295);
--color-brand-400: oklch(0.68 0.20 295);
--color-brand-500: oklch(0.59 0.22 295);
--color-brand-600: oklch(0.50 0.21 295);
--color-brand-700: oklch(0.42 0.18 295);
--color-brand-800: oklch(0.34 0.14 295);
--color-brand-900: oklch(0.26 0.10 295);
--color-brand-950: oklch(0.18 0.06 295);
}
That's the whole setup. The moment those variables exist, you can use bg-brand-500, text-brand-700, border-brand-200/40, and every other utility — with opacity modifiers, with arbitrary values, with everything Tailwind already supports. The naming convention is fixed: --color-{name}-{shade} becomes {utility}-{name}-{shade}.
0.59 to 0.50 looks like the same amount of "darker" no matter what hue you're working in. With HSL or hex, equal numerical steps produce wildly uneven visual steps — especially in yellows, greens, and cyans.This is the most common v4 question, so worth being explicit. There are two patterns:
| Goal | Pattern |
|---|---|
Add brand colors alongside the built-ins (blue-500 still works) | Define --color-brand-* in @theme. Done. |
Replace a default color (e.g. swap the built-in red for your own) | Redefine --color-red-500 etc. inside @theme. |
| Remove the entire default palette | Use @theme { --color-*: initial; } and then add only what you need. |
The initial trick is the v4-native equivalent of v3's theme vs theme.extend. Setting --color-*: initial wipes the entire color namespace, after which any subsequent --color-... declarations rebuild it from scratch.
Dark mode no longer needs a config flag. The dark: variant works out of the box; what you need to decide is whether you want media-query dark mode (follows the OS) or class-based (you toggle a class). For class-based:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-bg: oklch(0.99 0.00 0);
--color-bg-card: oklch(0.97 0.00 0);
--color-text: oklch(0.18 0.01 295);
--color-accent: oklch(0.59 0.22 295);
}
.dark {
--color-bg: oklch(0.13 0.01 295);
--color-bg-card: oklch(0.18 0.01 295);
--color-text: oklch(0.96 0.01 295);
--color-accent: oklch(0.72 0.20 295);
}
This is cleaner than the v3 approach because the theme tokens themselves are the variables. There's no second layer of indirection — bg-bg-card just resolves to whichever --color-bg-card is currently in scope. For more on building dark palettes that don't make your eyes bleed, our dark mode color palette guide covers contrast, elevation, and the near-black hue trick.
Because every token is exposed as a CSS variable, you can reach into the palette from anywhere — a hand-written stylesheet, an inline style, a charting library, a canvas element. No more importing resolveConfig from Tailwind in your JS bundle.
/* In any CSS file */
.chart-line { stroke: var(--color-brand-500); }
/* In a React component */
<div style={{ background: 'var(--color-brand-600)' }} />
/* In a canvas/JS context */
const accent = getComputedStyle(document.documentElement)
.getPropertyValue('--color-brand-500').trim();
This is one of the biggest practical wins of v4. Design tokens that used to be locked inside the build are now plain CSS variables that any tool can read at runtime — including SVG, charting libraries like D3 or Recharts, and CSS-in-JS shims you haven't fully retired yet.
If you're upgrading, your tailwind.config.js still works in v4 thanks to the back-compat layer — but new color additions belong in CSS. The cleanest migration is to move incrementally:
npx @tailwindcss/upgrade. It handles class renames (e.g. shadow-sm → shadow-xs) and rewrites the import.theme.extend.colors object and translate each entry into a --color-{name}-{shade} variable in @theme.#a855f7) and want the wider gamut, convert them to OKLCH. Most generators — including ours — will give you both formats side by side.tailwind.config.js only if you have plugins or content paths that need it — otherwise it can go too.| v3 | v4 |
|---|---|
theme.extend.colors = { brand: { 500: '#a855f7' } } | @theme { --color-brand-500: oklch(0.59 0.22 295); } |
colors: { ...defaultColors, brand } | Just add --color-brand-* — defaults stay unless you reset. |
darkMode: 'class' | @custom-variant dark (&:where(.dark, .dark *)); |
resolveConfig().theme.colors in JS | getComputedStyle(...).getPropertyValue('--color-brand-500') |
1. Forgetting the --color- prefix. A variable named --brand-500 won't generate utilities. The namespace prefix is what tells Tailwind "this is a color." Same logic applies to --spacing-*, --font-*, etc.
2. Defining colors outside @theme. A variable declared in :root is just a regular CSS variable. It won't produce bg-brand-500 classes. Only @theme declarations are scanned for utility generation.
3. Using hex when OKLCH would be cleaner. Hex still works in v4 — the framework auto-converts internally — but the built-in palette is OKLCH, so mixing the two means your custom colors won't interpolate smoothly with built-in ones (in gradients, in opacity blends, in color-mix()). If you're starting fresh, commit to OKLCH end-to-end.
app.cssHere is a production-grade Tailwind v4 starter with custom brand colors, semantic tokens for surfaces, and class-based dark mode — the kind of file you'd actually ship:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Brand */
--color-brand-50: oklch(0.97 0.02 295);
--color-brand-500: oklch(0.59 0.22 295);
--color-brand-600: oklch(0.50 0.21 295);
--color-brand-900: oklch(0.26 0.10 295);
/* Semantic surfaces (light) */
--color-surface: oklch(1.00 0.00 0);
--color-surface-muted: oklch(0.97 0.00 0);
--color-surface-border: oklch(0.90 0.00 0);
/* Semantic text */
--color-fg: oklch(0.18 0.01 295);
--color-fg-muted: oklch(0.50 0.01 295);
}
.dark {
--color-surface: oklch(0.13 0.01 295);
--color-surface-muted: oklch(0.18 0.01 295);
--color-surface-border: oklch(0.26 0.01 295);
--color-fg: oklch(0.96 0.01 295);
--color-fg-muted: oklch(0.70 0.01 295);
}
Drop that into app.css, import it once at the top of your app, and you're done. Every utility — bg-surface, border-surface-border, text-fg-muted, bg-brand-500/20 — works automatically in both modes.
Pick a base color, get a full 11-shade scale in OKLCH plus hex, ready to paste into your @theme block. Free, in-browser, no signup.
The official Tailwind docs at tailwindcss.com/docs/theme are the source of truth for the @theme directive and the full list of token namespaces. For the OKLCH side, the oklch.com picker is the easiest way to see what each lightness/chroma/hue triplet looks like before committing. For deployment ergonomics, our integration partner Namecheap bundles SSL, DNS, and CDN with one-click deploys so your new theme actually reaches a domain.