Color converter for Tailwind CSS: OKLCH, @theme, and shade scales
Tailwind v4 moved its colour system to OKLCH and the @theme directive in CSS replaced most of the JS config. Mapping a brand hex to a Tailwind palette is a different exercise than it was in v3, and the shape of the answer changed with it.
Tailwind v4 speaks OKLCH
Since v4 the default Tailwind palette is defined in OKLCH instead of hex or RGB. The reason is perceptual uniformity. A blue at lightness 0.5 looks as light as a red at lightness 0.5, which is not true in HSL or sRGB. The shade scale (50, 100, 200 ... 950) has consistent contrast steps across every hue.
Practical effect: the colours look more even when you swap one for another, the dark mode variants need less manual tweaking, and the contrast checker stops complaining at random points in the scale. Cost: OKLCH values do not look like anything you can read at a glance, you cannot tell a colour by squinting at oklch(0.65 0.18 250) the way you can with #3a86ff.
Mapping a brand hex to Tailwind v4 means converting it to OKLCH, picking the closest shade in the existing scale, and either using that shade as is or generating a custom 50 to 950 ramp around it.
From a single brand hex to a full ramp
The naive approach is to pick five lightness levels (90, 75, 60, 45, 30) at the same chroma and hue and call them 100, 300, 500, 700, 900. That works as a first pass and breaks at the extremes, the chroma needs to drop near pure white and pure black or the colour looks neon at the lightest end and muddy at the darkest.
A better recipe: start from the brand hex, convert to OKLCH, take the chroma as the peak. Build the ramp by holding the hue constant, dropping the chroma towards the ends (something like chroma * sin(pi * lightness) if you want a curve), and stepping the lightness from 0.97 (50) down to 0.20 (950). Tailwind's own palette uses a curve close to this.
For most projects you do not need to do this by hand. Tools (the converter on this site, plus colour ramp generators that target Tailwind) take a hex and emit the eleven OKLCH stops. Paste them into your @theme block and you have a custom palette that matches the rest of the framework's behaviour.
The @theme inline syntax
In v4 the canonical place for design tokens is @theme inside your CSS. The syntax declares CSS custom properties under specific Tailwind namespaces, and the framework picks them up automatically.
For a colour, the namespace is --color-. Define eleven of them and you get a palette named whatever you chose, with the standard 50 to 950 utilities (bg-brand-500, text-brand-900, etc.) generated for you.
The legacy JS config still works for migration but is on a deprecation path. New v4 projects ship a single CSS file with @import "tailwindcss"; followed by the @theme block, and the JS file disappears entirely. The block below is a complete brand colour ramp ready to paste into app.css or globals.css.
Shade aliases and dark mode
Once the ramp is in place, two more conventions tie up the system. The first is semantic aliases: --color-primary points at var(--color-brand-500) (or 600 if you want a darker default), --color-primary-hover points at 600 or 700, and your component code uses bg-primary instead of bg-brand-500. Renaming or rebranding is then a one line change.
The second is dark mode. The naive approach is to override every shade with its mirror (50 becomes the value of 950, 950 becomes the value of 50). It works but produces dim colours at the bright end. The cleaner pattern is to keep the ramp the same and only flip the semantic aliases: --color-primary points at 500 in light mode and at 400 in dark, because OKLCH 400 against a dark background has the same perceived contrast as 500 against a light one. The result is consistent visual weight in both modes without doubling the token count.
Working example
css/* app.css for Tailwind v4 */
@import "tailwindcss";
@theme {
/* Brand ramp generated from #3a86ff (h=255, c=0.18) */
--color-brand-50: oklch(0.97 0.02 255);
--color-brand-100: oklch(0.94 0.04 255);
--color-brand-200: oklch(0.88 0.08 255);
--color-brand-300: oklch(0.81 0.12 255);
--color-brand-400: oklch(0.72 0.16 255);
--color-brand-500: oklch(0.62 0.18 255); /* the brand colour */
--color-brand-600: oklch(0.54 0.17 255);
--color-brand-700: oklch(0.46 0.15 255);
--color-brand-800: oklch(0.38 0.12 255);
--color-brand-900: oklch(0.30 0.09 255);
--color-brand-950: oklch(0.22 0.06 255);
/* Semantic aliases, flipped per scheme */
--color-primary: var(--color-brand-500);
--color-primary-hover: var(--color-brand-600);
--color-primary-text: var(--color-brand-50);
}
@media (prefers-color-scheme: dark) {
:root {
--color-primary: var(--color-brand-400);
--color-primary-hover: var(--color-brand-300);
--color-primary-text: var(--color-brand-950);
}
}
/* Use in markup:
<button class="bg-primary text-primary-text hover:bg-primary-hover">
Sign in
</button>
*/ Just need the result?
Paste your brand hex into the colour converter on aldeacode.com and the OKLCH form appears alongside the eleven shade ramp. Copy the @theme block straight into your app.css, the palette matches the rest of Tailwind v4 by construction and dark mode is one alias flip away.
Open Color Converter (Hex / RGB / HSL) →Frequently asked questions
Do I still need a tailwind.config.js in v4?
Only for plugins, content paths the autodetect misses, or theme overrides that are easier in JS. Colours, fonts, spacing and breakpoints all live in @theme inside CSS now. Greenfield v4 projects often have no JS config at all.
Why OKLCH and not LCH or HSL?
OKLCH uses the OKLab perceptual model, which corrects the issues older CIE LCH had with blue. Browser support is universal as of 2023 (Safari 15.4 was the last hold out). HSL is still supported but is not perceptually uniform, the shade scale is less consistent across hues.
Can I keep using my existing hex palette in v4?
Yes. @theme accepts hex, rgb(), hsl(), oklch(), or any other CSS colour function. The palette will work, you just lose the perceptual uniformity that the default Tailwind palette gets from OKLCH. Mixing is fine for a migration.