DEV Community

Snappy Tools
Snappy Tools

Posted on

CSS Color Formats: A Practical Guide to HEX, RGB, HSL, and oklch

There is no single best way to write colors in CSS. Each format exists for a reason, and knowing when to use which one removes a category of micro-decisions from your workflow.

The basics: what are you actually writing?

Every CSS color format describes the same thing: a point in a color space, encoded as text the browser can parse. The difference is how that encoding works and what operations it makes easy.

HEX (#RRGGBB)

color: #2f855a;
background: #1a202c;
border-color: #e2e8f0;
Enter fullscreen mode Exit fullscreen mode

Structure: Three pairs of hexadecimal digits. Each pair represents one color channel (red, green, blue) on a scale from 00 (0) to ff (255).

Why it exists: Compact and universally supported. A hex code is shorter than its RGB equivalent and trivially copy-pasteable from any design tool (Figma, Sketch, Adobe XD all export hex by default).

3-digit shorthand: #fff expands to #ffffff. Works only when each pair repeats the same digit: #123 = #112233.

8-digit hex (with alpha): Add two more digits for opacity.

/* 50% transparent */
background: #2f855a80;

/* 80% opaque */  
background: #2f855acc;
Enter fullscreen mode Exit fullscreen mode

00 = fully transparent, ff = fully opaque. Use rgba() for older tool compatibility.

When to use: Any time you have a color from a design file. When you want compact, copy-pasteable color values. When you need a universally readable format across teams.

Limitation: Not intuitive to manipulate programmatically. To lighten #2f855a by 10%, you need to do the math or convert to HSL first.

RGB and RGBA

color: rgb(47, 133, 90);
background: rgba(26, 32, 44, 0.9);
Enter fullscreen mode Exit fullscreen mode

Structure: Three decimal numbers (0–255) for each channel, plus an optional fourth alpha value (0–1 as a decimal).

When to use:

  1. When reading computed styles from JavaScript. getComputedStyle() returns colors in rgb() format — matching that format simplifies comparison logic.

  2. When you need dynamic color assembly. Building color values from variables is readable in RGB:

const alpha = 0.8;
const r = 47, g = 133, b = 90;
element.style.background = `rgba(${r}, ${g}, ${b}, ${alpha})`;
Enter fullscreen mode Exit fullscreen mode

Limitation: Hard to imagine what rgb(200, 100, 150) looks like without a color picker. Not intuitive for design work.

HSL (Hue, Saturation, Lightness)

color: hsl(145, 48%, 35%);
background: hsla(145, 48%, 35%, 0.15);
Enter fullscreen mode Exit fullscreen mode

Structure: Hue is an angle (0–360°), saturation is a percentage, lightness is a percentage. hsl(145deg, 48%, 35%) works too — the deg unit is optional.

Color wheel positions: 0° = red, 60° = yellow, 120° = green, 180° = cyan, 240° = blue, 300° = magenta.

Why HSL is powerful for design systems:

:root {
  --brand: hsl(145, 48%, 35%);
  --brand-light: hsl(145, 48%, 90%);  /* same hue, lighter */
  --brand-muted: hsl(145, 20%, 35%);  /* same hue, less vivid */
  --brand-dark: hsl(145, 48%, 20%);   /* same hue, darker */
  --brand-hover: hsl(145, 48%, 28%);  /* same hue, slightly darker for hover */
}
Enter fullscreen mode Exit fullscreen mode

Without looking up a hex color, you know these are all related. The hue (145) stays constant; saturation and lightness shift. Try doing that with hex codes.

When to use: Building design systems where you need related color variants. Any time you want hover states, disabled states, or tint/shade relationships to be readable from the CSS itself.

Limitation: Perceptual uniformity is imperfect. Changing lightness by the same amount looks different at different hues — a yellow at L=50% looks brighter than a blue at L=50%. This is a real problem when building accessible color scales.

Modern CSS: oklch

color: oklch(55% 0.12 145);
Enter fullscreen mode Exit fullscreen mode

Structure: Lightness (0–100%), Chroma (0–~0.4), Hue (0–360°). Looks similar to HSL but the math underneath is different.

Why oklch exists: HSL's perceptual uniformity problem. oklch is built on the Oklab color space, designed so that equal changes in L actually look equally different — a property called perceptual uniformity. This makes it dramatically better for programmatic color generation:

/* These all look roughly the same "brightness" despite having different hues */
.red    { color: oklch(60% 0.2 0); }
.green  { color: oklch(60% 0.2 120); }
.blue   { color: oklch(60% 0.2 240); }
Enter fullscreen mode Exit fullscreen mode

With HSL at the same lightness value, green looks noticeably brighter than blue or purple. oklch fixes this.

Browser support: Chrome 111+, Firefox 113+, Safari 15.4+. Growing fast. For new design tokens, it is worth serious consideration.


Comparing the same color in every format

Here is #2f855a in all formats:

Format Value
HEX #2f855a
RGB rgb(47, 133, 90)
HSL hsl(145, 48%, 35%)
oklch oklch(52% 0.12 154)

They render identically. If you have a hex value and need to check HSL or oklch, an online color picker can convert between all formats instantly — useful for inspecting design tokens or adapting an existing palette to a new color model.


Quick decision guide

Situation Use
Color from Figma / Sketch / XD HEX
Assembling colors in JavaScript RGB
Design system tokens with variants HSL
New design system, modern browsers oklch
Transparency rgba() or 8-digit HEX
Print design (CSS is wrong here, use design tools) CMYK (not CSS)

Converting between formats

You rarely need to do this by hand. Options:

  1. Browser DevTools: Click any color swatch in the Styles panel and press Shift to cycle between hex, rgb, and hsl — fastest method when inspecting existing styles.

  2. Online converter: Paste a hex code into a color picker to see RGB, HSL, and CMYK equivalents instantly.

  3. In CSS: The color-mix() function lets you mix colors across spaces without manual conversion:

   color: color-mix(in oklch, hsl(145, 48%, 35%), white 20%);
Enter fullscreen mode Exit fullscreen mode
  1. In code (JavaScript):
   // Hex to RGB
   function hexToRgb(hex) {
     return {
       r: parseInt(hex.slice(1, 3), 16),
       g: parseInt(hex.slice(3, 5), 16),
       b: parseInt(hex.slice(5, 7), 16)
     };
   }
Enter fullscreen mode Exit fullscreen mode

Understanding which format to use and why eliminates a recurring decision from your workflow. Format consistency within a project matters more than which format you pick — choose one and stick to it for your design tokens.

Top comments (0)