turning a single brand color into a full Tailwind scale
A design team usually hands over exactly one hex value for the brand color, while Tailwind expects a complete scale from 50 to 950 for backgrounds, borders, text and hover states. Anyone who tries to click together those eleven steps by hand in a graphics tool rarely gets a consistent result, because lightness and saturation are hard to space evenly without a fixed color space. An OKLCH-based interpolation automates that process and produces scales that feel just as even as Tailwind's own built-in palettes.
Table of Contents
- 1. The problem: one brand color, but eleven steps needed
- 2. Why OKLCH instead of HSL for the interpolation
- 3. OKLCH syntax in CSS: lightness, chroma, hue
- 4. Planning the lightness/chroma curve for eleven steps
- 5. A simple generator script using the culori library
- 6. Ready-made tools instead of building your own: uicolors.app, Tailwind Shades and more
- 7. Integration into Tailwind v4's @theme
- 8. Contrast checking and manual fine-tuning
- 9. Common mistakes and a conclusion for your own workflow
- 10. Summary
- 11. FAQ
1. The problem: one brand color, but eleven steps needed
A client style guide typically contains just a single hex code for the brand color, say #0ea5e9 for a particular blue. Tailwind itself, though, consistently works with a scale of eleven steps, from 50 as the lightest background tone to 950 as an almost black text and contrast value. Each of these steps plays a fixed role in the UI: fifty and a hundred serve as subtle fill colors, five hundred usually marks the base value for buttons and links, and seven hundred through nine hundred deliver text with sufficient contrast on a light background.
If that scale is not derived cleanly from the brand color, cracks appear quickly: a hover state suddenly feels warmer or cooler than the base tone, or the darkest step unintentionally drifts toward violet instead of consistently heading toward a near-black blue. An automated generator solves this structurally by deriving all eleven steps from that single starting value according to one fixed, reproducible rule, instead of nudging them individually by hand.
2. Why OKLCH instead of HSL for the interpolation
The obvious approach would be to convert the hex value to HSL and shift the lightness component in even steps between light and dark. The problem: HSL lightness does not match how bright a color actually appears to the human eye. A pure yellow at fifty percent HSL lightness looks noticeably brighter than a blue at the same HSL lightness, which is why scales derived from HSL look nicely even for some hues and then show a visible brightness jump between two neighboring steps for others.
OKLCH solves exactly this problem, because its L channel is based on the perceptually uniform Oklab color space and actually tracks human brightness perception. Two colors with the same L value look equally bright to the eye, regardless of whether they are yellow, blue or red. That is precisely why Tailwind itself has used OKLCH as its internal color space for the built-in palettes since version four, and a custom generator that follows the same principle fits seamlessly into that system.
3. OKLCH syntax in CSS: lightness, chroma, hue
In CSS, an OKLCH color is written as oklch(L C H), where L expresses lightness between zero and one, C expresses chroma, meaning color saturation as a positive value with no fixed maximum, and H expresses hue as an angle between zero and three hundred sixty degrees. These three values can be varied independently, which gives exactly the control a color scale needs: hue stays largely constant across all eleven steps, while lightness and chroma are shifted deliberately toward light or dark.
It is important that chroma does not simply run linearly alongside lightness. Very light and very dark colors can only physically reach a limited saturation before falling outside the displayable sRGB gamut. A good generator automatically pulls back the chroma slightly near the edges of the scale, so the color is not clamped in the browser to an unwanted substitute value but instead stays controlled within the displayable range.
/* One step of the generated scale in CSS */
.swatch-500 {
/* L = 0.63, C = 0.19, H = 235 degrees (a blue hue) */
background-color: oklch(0.63 0.19 235);
}
/* For comparison: the same color as an sRGB fallback */
.swatch-500-fallback {
background-color: #0ea5e9;
}
4. Planning the lightness/chroma curve for eleven steps
Before a script gets to work, it is worth roughly fixing target lightness values for each of the eleven steps, modeled on Tailwind's own palettes. Step fifty typically sits around ninety seven to ninety eight percent lightness, step five hundred often falls somewhere between fifty and seventy percent depending on the starting color, and step nine hundred fifty lands around twelve to fifteen percent. These values form a curve, not a straight line, because the perceived brightness difference at the ends of the scale should be compressed more strongly than in the middle.
A similar logic applies to chroma: it rises from step fifty up to roughly step four or five hundred and then eases back down slightly, because very dark, strongly saturated colors quickly hit their limits in the sRGB space. A generator that exposes this curve as a configurable parameter, instead of hard-wiring it, can later be adapted easily to other brand colors with a completely different starting hue, without having to rewrite the core of the script.
5. A simple generator script using the culori library
For a practical implementation, the JavaScript library culori is a good fit, because it already ships conversions between hex, OKLCH and other color spaces, and it also handles gamut mapping for sRGB. A script reads in the brand hex color, converts it to OKLCH, takes its hue as a constant value for all steps, and computes a lightness/chroma pair for each of the eleven steps according to the previously planned curve.
The result can be output directly as a list of CSS custom properties, which are then copied into the Tailwind theme. It is important not to force the original brand hex value exactly onto step five hundred, but rather to identify the closest matching step and preserve the original chroma there, so the brand color stays recognizable in the scale instead of getting washed out by the interpolation.
import { formatHex, converter } from 'culori';
const toOklch = converter('oklch');
// Target curve: [L, C] per step, hue comes from the brand color
const CURVE = {
50: [0.98, 0.02], 100: [0.95, 0.045], 200: [0.90, 0.075],
300: [0.82, 0.11], 400: [0.72, 0.15], 500: [0.63, 0.19],
600: [0.54, 0.18], 700: [0.45, 0.15], 800: [0.36, 0.11],
900: [0.28, 0.08], 950: [0.16, 0.045],
};
function generateScale(brandHex) {
const base = toOklch(brandHex);
const scale = {};
for (const [step, [l, c]] of Object.entries(CURVE)) {
scale[step] = formatHex({ mode: 'oklch', l, c, h: base.h });
}
return scale;
}
console.log(generateScale('#0ea5e9'));
6. Ready-made tools instead of building your own: uicolors.app, Tailwind Shades and more
Not every project needs a hand-rolled script. The ecosystem already has several mature generators that accept a brand color as hex input and directly output a ready-made Tailwind configuration. uicolors.app shows the generated scale live as a preview with contrast values and exports either as a JavaScript object or as CSS custom properties for the v4 theme. Tailwind Shades follows a similar approach but additionally lets individual steps be nudged manually via sliders before exporting.
For teams who would rather adopt an established, already well thought-out step system, Radix Colors is worth a look: it defines twelve steps per base color with clearly documented intended usage, including separate scales for light and dark mode. The downside compared to a custom generator is that the exact brand color cannot always be matched one to one, because Radix starts from predefined reference tones instead of an arbitrary input value.
7. Integration into Tailwind v4's @theme
Tailwind v4 drops the classic JavaScript configuration file in favor of a CSS-first approach: design tokens are defined directly in the stylesheet inside an @theme block as CSS custom properties. The generated color scale lands there as a list of variables prefixed with --color-brand-, followed by the respective step, so for example --color-brand-500: oklch(0.63 0.19 235);. Tailwind automatically recognizes this naming scheme and generates all utility classes from it, such as bg-brand-500, text-brand-700 or border-brand-200, with no additional configuration required.
Because the values exist as real CSS custom properties, they can also be overridden at runtime, for instance for a second brand scheme in a multi-tenant setup or for an alternative dark mode color set. A generator script that outputs the finished @theme block directly as a text file saves the manual copy-paste step in practice and reduces the risk of swapping a step or introducing a typo in a variable name during the transfer.
8. Contrast checking and manual fine-tuning
An algorithmically generated scale is a very good starting point, but not a license to accept the contrast values unchecked. For body text on a white background, WCAG AA demands a contrast ratio of at least 4.5 to 1, which most generated scales only reach reliably from step six hundred or seven hundred onward. Every step actually intended for text in the project should therefore be checked individually against the planned background with a contrast calculator, instead of trusting the interpolation blindly.
In practice it also often turns out that the middle steps, especially four and five hundred, look a bit pale or too gray after automatic calculation when the starting color has an unusual hue and chroma combination. It is worth manually fine-tuning these two or three steps by hand while leaving the rest of the scale untouched from the generator. This hybrid approach, generating automatically and refining selectively by hand, delivers the most reliable results in practice.
9. Common mistakes and a conclusion for your own workflow
The most common mistake is linear interpolation in RGB or HSL instead of OKLCH, because it produces visible brightness jumps at certain points in the scale that feel slightly uneven in the finished UI. A second common mistake is failing to reduce chroma at the edges of the scale, which causes the browser to clip colors outside the sRGB gamut to an unwanted substitute value, making the lightest or darkest steps suddenly look different than planned. An undocumented script that nobody on the team can follow also quickly becomes a problem as soon as a second brand color is needed for a new project.
For your own workflow, a clear order is worth adopting: first roughly define the target curve for lightness and chroma based on Tailwind's own palettes, then generate automatically with culori or a comparable tool, next check contrast values for every text-relevant step, and finally fine-tune the two or three most critical steps by hand. Once this process is set up cleanly, it can be repeated for every new brand color in a few minutes instead of starting from scratch each time.
| Method | Perceptual evenness | Effort | Control over result |
|---|---|---|---|
| HSL interpolation | Low, visible brightness jumps depending on hue | Low | Barely controllable |
| OKLCH interpolation (script) | High, follows brightness perception | Medium, one-time script build | Fully controllable via curve |
| Ready-made tool (uicolors.app) | High, already OKLCH-based | Very low | Limited to tool options |
| Manual design per step | Depends on the designer | Very high | Maximum, but time-intensive |
Mironsoft
Tailwind CSS architecture, design systems, and performance
Tailwind frontends that stay maintainable despite thousands of utility classes?
We review existing Tailwind projects for bloated class lists, inconsistent design tokens, and unused CSS remnants, then build a design system that scales cleanly instead of getting messier with every component.
Design System Review
Checking tokens, spacing scale, and component consistency for maintainability.
Performance Optimization
Systematically reducing CSS bundle size, purge configuration, and load times.
Component Architecture
Building reusable, well-structured components instead of sprawling class lists.
10. Summary
Brand Color Palette Generation: The Essentials at a Glance
Color space for interpolation
OKLCH instead of HSL, since the L channel tracks actual brightness perception and avoids hue-dependent brightness jumps.
Number of steps
Eleven steps from 50 to 950, modeled on Tailwind's own naming convention and the intended use of each step.
Tooling
A custom script with the culori library for full control, or ready-made generators such as uicolors.app or Tailwind Shades.
Integration
Output directly as CSS custom properties inside Tailwind v4's @theme block, prefixed with --color-brand-.