Tailwind CSS v4 Color Palette in 2026 — How to Build, Customize and Theme With Custom Colors

Updated May 16, 2026 · 10 min read · By the TinyTools team

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.

Need a base palette first?

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 →

What actually changed in v4

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:

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.

The minimum viable v4 color setup

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);
}
50
100
200
300
400
500
600
700
800
900
950

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}.

Why OKLCH: the lightness axis in OKLCH is perceptually uniform, so stepping from 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.

Replacing vs extending the default palette

This is the most common v4 question, so worth being explicit. There are two patterns:

GoalPattern
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 paletteUse @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 in v4

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.

Reading theme colors from any CSS or JS

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.

Migrating a v3 palette to v4

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:

  1. Run npx @tailwindcss/upgrade. It handles class renames (e.g. shadow-smshadow-xs) and rewrites the import.
  2. Take your theme.extend.colors object and translate each entry into a --color-{name}-{shade} variable in @theme.
  3. If you defined custom colors as hex (#a855f7) and want the wider gamut, convert them to OKLCH. Most generators — including ours — will give you both formats side by side.
  4. Delete the JS color config. Keep tailwind.config.js only if you have plugins or content paths that need it — otherwise it can go too.

v3 to v4 cheat sheet

v3v4
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 JSgetComputedStyle(...).getPropertyValue('--color-brand-500')

Three pitfalls people hit on day one

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.

A complete starter app.css

Here 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.

Generate your starter palette in seconds

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.

Open the Color Palette Generator →

Further reading

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.