This content originally appeared on DEV Community and was authored by Matteo Frana
Overview
This article explores how Tailwind CSS v4 has adopted OKLCH color notation and why this change is significant for web developers and designers. You'll discover the fundamentals of color spaces, learn why OKLCH offers better color manipulation than HSL, and understand how to replicate Tailwind's color system for your own custom colors.
Introduction
Tailwind CSS recently released v4 with several new features. A key change is that colors are now defined using OKLCH notation, such as oklch(0.685 0.169 237.323)
. I had never encountered this color notation before and I saw that it enabled more vivid colors. My nerd curiosity kicked in, leading me to explore high-gamut color spaces and discover why OKLCH notation is better than HSL.
During this time, I also needed to create custom colors for a Tailwind CSS project. This presented the perfect opportunity for learning and some late-night hacking sessions. The result was uihue.com. In this article, I'll share what I learned and explain the algorithm I developed to generate color hues.
A bit of Color Theory
The CSS color module level 4 specification introduces new syntax for expressing colors in the standard sRGB color space with rgb(…)
or hsl(…)
. The modern syntax separates color components with spaces instead of commas, uses a slash for the optional alpha value, and allows mixing percentages with numbers. For example, rgba(255, 0, 0, 0.5)
becomes rgba(100% 0% 0% / 50%)
.
More significantly, this specification introduces new ways to define colors using different color spaces. But what exactly is a color space?
Understanding Color Spaces: how we see and define colors
Let's start with what a color is. A color is our visual perception of matter based on its electromagnetic spectrum (the amount of light at each visible frequency that travels from an object to our eyes). For objects that don't emit light, color depends on the spectrum of the light hitting them and how they absorb and reflect it. For objects that do emit light, we must also consider their emission spectrum—how much light they emit at each visible frequency.
The CSS specification defines color as "a definition (numeric or textual) of the human visual perception of a light or a physical object illuminated with light." For the specification's purposes, what matters is how a color can be expressed as a string or number.
In this regard, the specification defines a color space as "an organization of colors with respect to an underlying colorimetric model, such that there is a clear, objectively measurable meaning for any color in that color space." A single color can be expressed in different color spaces, though some colors may only be represented in certain color spaces.
Color Gamut: from sRGB to Modern Display Standards
No display can reproduce all the colors that the human eye can perceive. The range of colors a display can produce is called a "gamut." Most modern displays show colors in the "sRGB" color space's gamut, which covers about 35% of all human-visible colors. These colors can be expressed using the rgb(…)
or hex notation (e.g. #f65a8e
).
Modern devices, particularly Apple computers and phones, can display a broader range of colors—usually more saturated ones. This capability requires new color spaces and notation forms to express these wider gamuts.
Color spaces, arranged from smallest to largest gamut, are: sRGB ⇒ Display P3 ⇒ Adobe RGB 98 ⇒ REC.2020 ⇒ ProPhoto RGB. Current Apple devices can usually display colors up to the REC.2020 color space gamut.
Color Space Components and Notation
Colors in a CSS color space notation are represented as a list of color components (also called "channels") that represent components along the axes in the color space. Each channel has a minimum and maximum value, and any color with values outside these ranges is considered invalid.
Note about Alpha
We won't discuss the additional alpha component, which controls transparency, as it can be considered a post-processing operation that blends a color with whatever is beneath it.
Examples of color notations:
-
rgb()
defines colors in the sRGB color space using red, green, and blue channels -
hsl()
defines colors in the sRGB color space using hue, saturation, and lightness in the HSL cylindrical coordinate model
CSS level 4 introduces additional color notations: hwb
for sRGB, lab
and lch
for CIELAB, and oklab
and oklch
for Oklab, along with a general color
function for various color spaces.
In this article, we'll focus on oklch—the most relevant option and the one Tailwind v4 uses for its predefined colors. We won't delve into technical topics like the differences between CIE Lab and Oklab color spaces or cartesian versus cylindrical coordinates.
OKLCH: A Better Way to Express Color
The OKLCH color notation is based on the Oklab color space, designed to enhance perceptual uniformity, hue and lightness prediction, and color blending. It was introduced by Björn Ottosson in December 2020. OKLCH represents colors using cylindrical coordinates in the Oklab color space.
Here are the three key aspects of the OKLCH notation:
-
It allows colors to be expressed in the P3 or REC.2020 gamut—colors beyond what
rgb()
,hsl()
, or hex formats can represent. This future-proof notation can even define colors that current devices cannot yet display. - Its coordinates are:
- L (Perceived Lightness): ranges from 0 to 1, or 0% to 100%
- C (Chroma, or saturation): ranges from 0 to infinity (but it always stays below 0.37 in the P3 color space)
- H (Hue): ranges from 0 to 360 degrees
- It uses perception-based lightness and saturation, solving major perceptual issues found in HSL notation and enabling easier color manipulation (see next section)
HSL's Perceptual Limitations
Let's explore the two major perceptual limitations of HSL and the sRGB color space.
Variable Maximum Saturation
In HSL, the maximum saturation remains constant across all hues. However, in reality, maximum saturation varies depending on both hue and lightness—this applies to both displays and the human eye.
I recommend checking out the excellent OKLCH color picker at oklch.com. Be sure to enable the "Show 3D" switch (because who doesn't love going full nerd?) to view the 3D model of the color space. At the "base" of these "color mountains" you'll find a Hue/Lightness diagram with zero Chroma (showing only grays), while the mountains' heights represent the maximum Chroma available across different hues and lightness levels.
You can see from these two images of the 3D OKLCH color space (from oklch.com) how greens have a higher maximum chroma compared to yellows and how blue reaches its highest chroma at low lightness levels.
Non-Uniform Lightness in HSL
A critical issue with HSL is that its lightness values don't align with human visual perception across different hues. Two colors can have the same HSL lightness value yet appear completely different in brightness to our eyes.
The Oklch notation in the Oklab color space solves this problem by ensuring that identical lightness values create the same perceived brightness.
The example below illustrates this difference: colors with the same HSL lightness can look dramatically different in brightness. However, when we change only the hue in Oklch while keeping the lightness constant, all colors appear equally bright.
OKLCH in Practice: Three Game-Changing Benefits
Better Color Manipulation
As we saw, OKLCH ensures that Lightness and Chroma values are perceptually consistent across all hues. This enables better mathematical color manipulation. When you need a specific lightness level to ensure sufficient contrast with white text, OKLCH provides reliable results, unlike HSL. This makes color adjustment functions like darken/lighten more reliable.
Better Color Gradients
While HSL gradients tend to create unwanted gray areas when colors mix, OKLCH's perceptually uniform model produces smooth, visually balanced gradient transitions.
Wider Gamut
OKLCH lets us express and use colors with a higher gamut, producing more vibrant, eye-catching designs that modern devices can display. While the image on the left appears more saturated, it has been converted to sRGB due to Dev.to's image processing, but it still illustrates the concept.
Browser Compatibility and Fallback Strategy
With all these advantages of OKLCH in mind, you might wonder about browser support. The good news is that as of early 2025, OKLCH enjoys full support across all major modern browsers. And when colors fall outside a display's gamut, browsers automatically handle the conversion to a supported gamut.
Decoding Tailwind's Color Architecture
Do you know how many color types (different hues) Tailwind includes? You might guess 10 or 12? Maybe 15?
Actually, there are 22! They are: Red, Orange, Amber, Yellow, Lime, Green, Emerald, Teal, Cyan, Sky, Blue, Indigo, Violet, Purple, Fuchsia, Pink, Rose, Slate, Gray, Zinc, Neutral, and Stone—plus black and white, making it 24 in total.
Each of these color types has a palette of 11 different shades (50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950). The 50 shade represents the lightest color, while 950 represents the darkest.
Before Tailwind v4, these colors were defined in the sRGB color space using HSL or HEX format. With Tailwind v4, colors are now defined using the OKLCH format in the Oklab color space. This change allowed Tailwind expert designers to choose colors outside the sRGB gamut—in fact, you will find some more saturated colors that exist only in the P3 gamut.
Anatomy of a Tailwind Color Palette
You might expect the hue and saturation to remain constant across the whole palette, and the lightness to decrease linearly from 50 to 950—especially since the Oklab model is perceptually uniform, right?
I am sorry to disappoint you: all these assumptions are wrong. You can see the charts for all the new Tailwind colors, showing how lightness, chroma, and hue change across different shades, here: https://www.uihue.com/tailwind-colors-charts
Here are three examples:
As you can see, none of these diagrams follows a linear pattern. I stumbled upon this surprising discovery while trying to replicate Tailwind's palette-building approach for custom color palettes.
Don't worry though—let's try to break down the underlying patterns.
Lightness
The Lightness follows a non-linear curve that remains fairly consistent across color palettes, though grays show a steeper decrease in the darker shades.
Chroma
The chroma (saturation) follows a Gaussian curve pattern, peaking between the "400" and "600" shades, with a sharper decrease in the lighter shades (toward the left side).
Hue
In the charts, hue values have been normalized by subtracting the minimum hue value. Without this normalization, hues near 0° (like reds) would show larger min-max differences than higher hues (like violet). This normalization allows you to better compare hue variations across the entire spectrum.
Looking at all the charts, you'll notice that hue remains fairly consistent across different shades, with two notable exceptions: yellows shift toward orange in darker shades, and blues shift toward violet in darker shades.
From Analysis to Algorithm: Recreating Tailwind's Colors
My mathematical mind's first instinct was to derive three average curves—for lightness, chroma, and hue—across all colors (maybe with separate rules for colors and grays). I planned to use a Fourier transform to approximate these curves and create a clean rule for generating new colors.
However, this approach wouldn't work. I wanted to achieve the highest possible fidelity in replicating Tailwind colors, but averaging would have flattened the subtle hue differences that make each palette special. I would have lost crucial nuances, like how yellows become more orange when darker, or how azure shifts toward blue-violet. Then a much simpler idea struck me.
I could simply identify the nearest Tailwind color for any given input color and apply the same rules that Tailwind uses for that nearest color. So, the first step was to find the nearest Tailwind color to the user's chosen color.
Finding the Nearest Color
Given a collection of colors and a target color, how do we determine which color in the collection is closest to our target? Simple: we test each color, measure the distance, and choose the one with the minimum distance. But this raises another question: how do we define the distance between two colors?
The simplest approach uses Euclidean distance across the N axes of the color representation (essentially applying the Pythagorean theorem in N dimensions). In the sRGB color space, for example, we could calculate the Euclidean distance using the R, G, and B axes:
Unfortunately, this distance calculation isn't very effective. Consider the example in the following image: blue and violet have a greater Euclidean distance than yellow and orange, even though they appear much more similar to our eyes.
If we switch to a perceptually uniform color space, such as Lab, the Euclidean distance becomes much more reliable.
I then learned about a family of algorithms called "DeltaE" (DeltaE) (ΔE), specifically designed to calculate the difference between two colors. The first version, the CIE 1976 formula, simply used the Euclidean distance of colors in the Lab color space. However, when Lab proved less perceptually uniform than initially thought, the algorithm went through several revisions in 1984, 1994, and finally 2000—resulting in the most accurate, though most complex, Lab-based DeltaE algorithm to date.
I used the DeltaE 2000 algorithm implementation from the Colorjs.io library to iterate through the Tailwind CSS colors and find the nearest match. I'm also interested in testing a simpler Euclidean distance calculation in a more advanced color space like JzCzhz.
Generating Color Palettes: The Algorithm
Now that we have the nearest Tailwind color (let's say it's "sky-700"), we can proceed with generating a complete palette.
The user's selected color becomes the "700" shade in the palette—our "base shade." From there, we need to generate both lighter and darker shades.
A simple approach would be to take the hue of the user's selected color and apply the same lightness and chroma values from the nearest Tailwind color for each shade.
However, this would cause us to lose the unique characteristics of the user's color, for example lower saturation or slightly lower lightness compared to the Tailwind color.
Simply applying Tailwind's lightness and chroma deltas from the base shade seemed promising, but this approach could produce completely desaturated colors at the extreme ends of the palette.
Instead, I applied these deltas with a smoothing effect as we approach the extreme light and dark hues. This preserves the color's distinctive features where they matter most—in the middle shades—while ensuring balanced results for the lightest and darkest shades.
Name that Color!
As developers, we know naming things is one of the hardest tasks. When I needed to give each user's color pick in uihue a beautiful name, I faced quite a challenge.
Initially, I considered using the standard HTML 4.01 named colors like "red," "lime," "aliceblue," or "papayawhip." But with only 148 named colors available, this wouldn't provide enough unique names for the vast spectrum of possible colors.
I then found a massive list containing over 30k colors. However, calculating 30k color distances for each color pick would be unnecessarily resource-intensive. Instead, I settled on using NTC colors—a collection of 1,566 names that provides enough variety to find beautiful color names without excessive CPU usage.
User-Friendly OKLCH Color Selection
As far as I know, there's just one dedicated color picker for the OKLCH color space: the excellent tool at oklch.com. While it's really great and features the neat 3D representation of the color space, this kind of interface might intimidate users who aren't familiar with how OKLCH works.
Instead, I decided to implement a standard HSL color picker in the sRGB color space, convert the color to OKLCH, and then let users increase the chroma with a simple slider. The interface clearly shows when colors enter higher gamuts like P3 or REC2020.
I'm very satisfied with the result: users find it both user-friendly and fun to use.
Implementing Accessible Color Contrast
In the uihue.com app, I display color shade numbers over the colored squares of the generated palette, using either light or dark text.
How do I determine which text color provides better contrast against each shade's background? Simple—I measure the contrast ratio for both options and choose the higher one.
As you might expect, there's science behind this. Several algorithms exist for calculating contrast between text and background colors, including: Weber contrast, Michelson contrast, Advanced Perceptual Contrast Algorithm (APCA), Lightness difference in the CIE Lightness L* space, Delta Phi Star (ΔΦ*), and WCAG 2.1.
I chose to implement the APCA algorithm (through the Colorjs.io library), as it offers the best performance and is being considered for inclusion in version 3 of the W3C Web Content Accessibility Guidelines (WCAG).
Conclusion
In this journey through color spaces and Tailwind's color system, we've explored the advantages of OKLCH over traditional color notations, particularly in web development. The transition to OKLCH in Tailwind v4 represents a significant step forward in how we handle colors in modern web design, offering better perceptual uniformity, wider gamut support, and more reliable color manipulations.
As display capabilities improve, the OKLCH color space will become increasingly important. It enables us to create more vibrant, accessible, and visually appealing designs while maintaining precise control over color relationships and contrast ratios.
Through the development of uihue.com, we've seen how understanding color spaces and implementing smart color-matching algorithms can bridge the gap between technical color theory and practical web development needs. The complexity behind Tailwind's carefully crafted color palettes reveals that even seemingly simple color choices involve intricate patterns and thoughtful design decisions.
References
- uihue: Tailwind CSS v4 Oklch Palette Generator
- OKLCH Color Picker & Converter
- CSS Color Module Level 4
- Björn Ottosson - A perceptual color space for image processing
- Evil Martians - OKLCH in CSS: why we moved from RGB and HSL
This content originally appeared on DEV Community and was authored by Matteo Frana

Matteo Frana | Sciencx (2025-02-20T19:07:56+00:00) The Mystery of Tailwind Colors (v4). Retrieved from https://www.scien.cx/2025/02/20/the-mystery-of-tailwind-colors-v4/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.