The Minimum You Need to Know About Colors in CSS

After countless articles, videos, and course lessons about colors in CSS, I’ve summarized in this article everything I consider the minimum you need to know about colors in CSS.

Most devs still use the most basic forms of colors: hexadecimal and RGB.

However, CSS Colors Level 4, published in July 2022, introduced major improvements that allow, for example: using colors outside the sRGB gamut on modern displays, creating perceptually uniform color scales, and manipulating existing colors with relative color syntax.

In this article, I’ll show how to use these new color functions and create better, more consistent color systems.

Basic Concepts: Hue, Saturation, Luminosity, and Color Spaces

Before we start understanding colors, we need to understand these basic concepts that are the foundations for building them.

Hue is the “tone” of the color. It represents the color’s position on the color wheel, measured in degrees from 0° to 360°. For example: 0° is red, 120° is green, 240° is blue, and 360° returns to red. It’s basically “which color” you want.

It’s important to note that hue values vary according to the color space. The same hue angle produces different colors in HSL versus OKLCH. For instance, pure red is at 0° in HSL but around 30° in OKLCH. This happens because each color space organizes hues differently based on its underlying mathematical model:

HSL Hue (0° to 360°)

OKLCH Hue (0° to 360°)

Color Space is the system that defines how colors are represented and organized. Think of it as a “map” of all possible colors within that system. RGB, HSL, and OKLCH are examples of different color spaces. Each one has its own way of describing colors and its own gamut (range) of colors it can represent. Because of this, the same color can have different values depending on the color space used, and some color spaces can represent more vibrant colors than others.

Gamut is the range of colors that a color space or device can represent. sRGB (used by most monitors) has a smaller gamut than Display P3 (used in recent Apple devices). OKLCH can describe any color visible to the human eye, but not all of them can be displayed on every monitor. When you specify a color outside the device’s gamut, the browser automatically performs “gamut mapping” to find the closest displayable color.

Saturation is the purity of the color. The more saturated, the more vivid the color is, meaning it has more of the color itself. The less saturated, the more “gray” it becomes. A saturation of 0% results in a shade of gray, while 100% is the color in its purest form.

Luminosity is the amount of light a color has. The more luminous, the closer to “white” the color is. The less luminous, the closer to “black.”

Named Colors

Named colors are colors defined by their name. Example: color: red, color: blue, color: coral.

Obviously, we don’t need to delve into the reasons why we shouldn’t use them in production. They are limited (about 140 colors), but they’re useful for quick tests or tutorials.

Hexadecimal and RGB

Hexadecimal and RGB are probably the most common ways to define colors in CSS today.

HEX is composed of 6 digits, with the first 2 for red, the next 2 for green, and the last 2 for blue. Each pair ranges from 00 to FF (0 to 255 in decimal). Through this mix of values, you create the color.

Example: #FF5733 → FF (red), 57 (green), 33 (blue).

RGB works the same way, but uses the rgb() function that receives the red, green, and blue values. Each number between 0 and 255 represents the intensity of that color channel.

To add transparency, we can add a fourth value: rgba(255, 87, 51, 0.5), where the last value is the alpha (0 = transparent, 1 = opaque).

It’s worth noting that the CSS specification accepts different syntaxes, and we no longer need to use the function variant with “a” for alpha:

.red {
  color: rgb(255, 0, 0, 1); /* with comma */
  color: rgb(255 0 0 / 1); /* without comma */

  /* The same is valid for HSL */
  color: hsl(0, 100%, 50%, 1); /* with comma */
  color: hsl(0 100% 50% / 1); /* without comma */
}

Problem: looking at #a3b2c1 or rgb(163, 178, 193), could you roughly infer what color it is? Very unlikely.

Modern editors visually highlight the color, which helps. However, since the values are still incomprehensible to humans, it’s very difficult to use modern CSS features like relative colors or create logical color variations.

For example: given the color #a3a3a3, can you tell that it’s 30% lighter than another color? Or easily create a darker version? Probably not.

HSL

HSL is a way of defining colors that solves the problems above. Instead of defining colors by mixing light channels, you define the hue, saturation, and luminosity.

Syntax: hsl(hue, saturation, luminosity)

Example: hsl(240, 100%, 50%). This color has hue 240° (blue), 100% saturation (pure color), and 50% luminosity (neither light nor dark).

For transparency, we use HSLA: hsla(240, 100%, 50%, 0.5).

With this, we can create logical color variations! Here’s an example of a primary color scale:

Primary color scale with HSL (varying luminosity)

95%
85%
70%
55%
40%
25%
15%

Although this works in theory, there’s a practical problem. You’d expect that changing only the hue while maintaining saturation and luminosity would generate colors with the same perceived brightness.

However, that’s not what happens:

HSL colors with same saturation (100%) and luminosity (50%)

Red 0°
Yellow 60°
Green 120°
Cyan 180°
Blue 240°
Magenta 300°

Notice how yellow appears much brighter than blue, even with the same luminosity value.

Even though all these colors have the same luminosity and saturation, yellow appears much brighter than blue. This happens because HSL’s mathematical model is not optimized for human perception.

It’s not a critical problem, but it’s something to keep in mind when creating consistent design systems.

In the CSS Color Level 4 specification, a new way of defining colors was introduced that solves this problem.

OKLCH

OKLCH works similarly to HSL, but was designed to be perceptually uniform. This means that two colors with the same luminosity really appear to have the same brightness to human eyes.

See the comparison below:

OKLCH colors with same luminosity (65%) and chroma (0.15)

Red 30°
Yellow 90°
Green 150°
Cyan 200°
Blue 260°
Magenta 320°

Now all colors appear with similar perceived brightness!

Syntax: oklch(luminosity chroma hue)

  • Luminosity: 0% to 100%
  • Chroma: color intensity, generally from 0 to ~0.4 (it’s not a percentage!)
  • Hue: 0 to 360 degrees (note: hue values differ from HSL — red is around 30° in OKLCH, not 0°)

Example: oklch(50% 0.2 260) — 50% luminosity, chroma 0.2, hue 260° (blue).

For transparency: oklch(50% 0.2 260 / 0.5).

Chroma vs Saturation: the difference is subtle but important. Saturation is relative to luminosity, while chroma is an absolute measure of “colorfulness.” In practice, chroma in OKLCH produces more consistent results across different colors.

With OKLCH, we can create color scales by varying just one property — luminosity — while keeping chroma and hue constant. The result is a scale with consistent perceived brightness at each step:

Primary color scale with OKLCH (varying only luminosity, chroma fixed at 0.15)

95%
85%
70%
55%
45%
35%
25%

Now, different colors with the same luminosity will appear equally bright!

A note on gamut: when using high chroma values at extreme luminosities (very light or very dark), some colors may fall outside the sRGB gamut. The browser will automatically map these to the closest displayable color. To ensure predictable results, use moderate chroma values (around 0.1 to 0.2) or check your colors with a tool like oklch.com.

Browser Support

OKLCH has been supported in all major browsers since May 2023, with approximately 93% global coverage. You can check current support at caniuse.com/mdn-css_types_color_oklch.

For older browsers, you can provide a fallback:

.button {
  background: #3b82f6; /* fallback for older browsers */
  background: oklch(60% 0.22 250);
}

Relative Colors

A powerful feature of modern CSS is the ability to create colors relative to other colors. This allows you to manipulate existing colors without having to redefine them from scratch.

Syntax: hsl(from base-color h s l) or oklch(from base-color l c h).

This allows us to define base colors once and generate all variants programmatically. Let’s see an example.

Imagine you have primary and secondary buttons with different colors. You want to add a hover state, which is generally darker than the base color.

Before relative colors, you would have to define a new color for the hover state:

/* Default button is primary */
button {
  background: var(--primary-color);

  &:hover {
    background: var(--primary-color-dark);
  }
}

But with relative colors, you can define a single color and generate all variants programmatically:

/* Primary button */
button {
  --button-base-color: var(--primary-color);

  background: var(--button-base-color);
  border: 1px solid oklch(from var(--button-base-color) calc(l - 0.1) c h); /* Darken the base color by 10% */

  &:hover {
    background: oklch(from var(--button-base-color) calc(l - 0.1) c h);
  }
}

/* Secondary button */
button.secondary {
  --button-base-color: var(--secondary-color); /* Just set the variable once! */
}

This unlocks even more possibilities, because now we’re not limited to predefined variants. If you need a button with a color other than primary or secondary, you can set the variable to any color you want:

<button style="--button-base-color: #f00">Red button</button>

Result:

Hover over each button to see the automatically generated darker state. Notice how the border and the hover state are always darker than the background — all calculated automatically from a single color variable!

More Things I Didn’t Cover in This Article

There’s a lot more incredible stuff that I didn’t cover here. Feel free to dive deeper:

  • light-dark() — a function that lets you define two color values (one for light mode, one for dark mode) and automatically picks the right one based on the user’s color-scheme, eliminating the need for media queries in many cases.
  • color-mix() — a function that mixes two colors together in a given color space by a specified percentage, useful for creating intermediate tones or adjusting colors without doing the math yourself.
  • lab, oklab, lch, and other CSS color functions.

References

  1. CSS Color Level 4
  2. oklch.com
  3. CSS for JavaScript Developers — Josh Comeau Course
  4. A pragmatic guide to modern CSS colours - part one
  5. A pragmatic guide to modern CSS colours - part two