ToolSite
All posts

HEX vs RGB vs HSL: Understanding Color Formats in CSS

HEX, RGB, and HSL: when to use each color format in CSS. Understand how they work, their strengths, and convert between them with our free color converter.

By ToolSite6 min readguides

Three Ways to Say the Same Color

The color teal can be expressed in CSS in all three formats:

/* HEX */
.teal-hex  { color: #008080; }

/* RGB */
.teal-rgb  { color: rgb(0, 128, 128); }

/* HSL */
.teal-hsl  { color: hsl(180, 100%, 25%); }

All three produce the same color on screen. They differ in how you arrive at the values and when each format is easier to reason about. Understanding the trade-offs helps you pick the right one for the task.

HEX

Hexadecimal colors are the most compact format. Six hex digits, optionally preceded by #:

#RRGGBB

Each pair of digits represents red, green, and blue intensity from 00 (0, none) to FF (255, full). #FF0000 is pure red. #000000 is black. #FFFFFF is white.

Three-digit shorthand is valid when each pair shares the same digit: #F00 equals #FF0000 (red), #0F0 equals #00FF00 (green), #333 equals #333333 (dark gray).

An 8-digit variant includes alpha transparency. #FF000080 is red at 50% opacity. This is useful in design tokens for semi-transparent variants:

:root {
  --surface-overlay: #00000040;  /* 25% black overlay */
  --brand-ghost:    #3B82F620;  /* 12% brand tint */
  --shadow-subtle:  #0000001A;  /* 10% black shadow */
}

HEX is the most common format in CSS because it is compact and designers copy-paste it directly from Figma, Photoshop, and color pickers. The downside: HEX values do not map intuitively to how humans think about color. #008080 is teal, but you cannot tell that from the digits. Making it "a bit lighter" requires mental hex math.

RGB

RGB uses decimal values (0 to 255) for red, green, and blue:

/* Legacy syntax */
.legacy   { color: rgb(0, 128, 128); }
.legacy-a { color: rgba(0, 128, 128, 0.5); }

/* Modern syntax (space-separated, no commas) */
.modern   { color: rgb(0 128 128); }
.modern-a { color: rgb(0 128 128 / 0.5); }

RGB is straightforward: pick how much red, green, and blue you want. It matches the native format of screens since each pixel is a combination of red, green, and blue sub-pixels. The downside: adjusting a color by eye is unintuitive. To make teal "lighter but less saturated," you adjust three numbers simultaneously without knowing which direction to go.

Where RGB shines is programmatic manipulation. In JavaScript, you can read and write individual color channels:

const el = document.querySelector('.card');
const style = getComputedStyle(el);
const bg = style.backgroundColor;  // "rgb(0, 128, 128)"

// Parse individual channels
const [r, g, b] = bg.match(/\d+/g).map(Number);

// Increase red channel by 30 for a warmer variant
el.style.backgroundColor = `rgb(${r + 30}, ${g}, ${b})`;

This channel-level access is impossible with HEX strings unless you parse them first. CSS animations also work naturally with RGB:

@keyframes pulse-red {
  0%   { background-color: rgb(255, 50, 50); }
  50%  { background-color: rgb(255, 100, 100); }
  100% { background-color: rgb(255, 50, 50); }
}

HSL

HSL describes colors the way humans perceive them:

hsl(hue, saturation%, lightness%)
  • Hue: the base color on the color wheel, 0 to 360 degrees. 0 is red, 120 is green, 240 is blue, 360 wraps back to red.
  • Saturation: how intense the color is. 0% is gray (no color). 100% is full color.
  • Lightness: how bright. 0% is black. 50% is the pure color. 100% is white.

Teal is hsl(180, 100%, 25%). To make it lighter, increase lightness to 35%. To make it less saturated, decrease saturation to 70%. These are single-value adjustments that match the words designers use.

HSL with alpha follows the same modern syntax:

/* Legacy */
.legacy { color: hsla(180, 100%, 25%, 0.5); }

/* Modern */
.modern { color: hsl(180 100% 25% / 0.5); }

When to Use Each

FormatUse When
HEXQuick copy-paste from design tools. Compact commit diffs. Familiar to every developer.
RGBYou need channel-level access in JavaScript. Color animations that transition smoothly. Dynamic color generation.
HSLYou design in code and adjust colors intuitively. Building systematic color scales. Dark mode overrides.

Building a Design-System Palette with HSL

The killer feature of HSL: generate an entire color scale by varying one parameter. Here is a full blue palette where only lightness changes:

:root {
  --blue-50:  hsl(210, 80%, 95%);
  --blue-100: hsl(210, 80%, 88%);
  --blue-200: hsl(210, 80%, 78%);
  --blue-300: hsl(210, 80%, 65%);
  --blue-400: hsl(210, 80%, 55%);
  --blue-500: hsl(210, 80%, 50%);  /* base */
  --blue-600: hsl(210, 80%, 42%);
  --blue-700: hsl(210, 80%, 32%);
  --blue-800: hsl(210, 80%, 22%);
  --blue-900: hsl(210, 80%, 15%);
}

Swap 210 (blue) for 340 (pink), 45 (gold), or 160 (emerald) and you have an instant palette for any brand color. This is how Tailwind CSS and other utility-first frameworks build their color scales.

For accessible contrast, pair lightness values with at least 40 points of separation. --blue-100 (88% lightness) on --blue-800 (22% lightness) passes WCAG AA contrast requirements.

You can also create semantic tokens on top of the scale:

:root {
  --color-text-primary:    var(--blue-900);
  --color-text-secondary:  var(--blue-600);
  --color-surface:         var(--blue-50);
  --color-border:          var(--blue-200);
  --color-accent:          var(--blue-500);
}

HSL for Dark Mode

HSL separates hue from lightness, which makes dark mode overrides trivial. Instead of rewriting every color, shift only the lightness values:

:root {
  --color-bg: hsl(210, 30%, 98%);
  --color-text: hsl(210, 30%, 15%);
}

[data-theme="dark"] {
  --color-bg: hsl(210, 30%, 12%);
  --color-text: hsl(210, 30%, 90%);
}

The hue and saturation stay the same. Only the lightness flips. With HEX or RGB you would need to compute entirely new values for each token.

For more complex systems, decompose HSL into individual custom properties so you can override one dimension at a time:

:root {
  --hue: 210;
  --saturation: 80%;

  --color-primary: hsl(var(--hue), var(--saturation), 50%);
  --color-primary-light: hsl(var(--hue), var(--saturation), 70%);
  --color-primary-dark: hsl(var(--hue), var(--saturation), 30%);
}

[data-theme="dark"] {
  --color-primary: hsl(var(--hue), 60%, 65%);
  --color-primary-light: hsl(var(--hue), 60%, 80%);
  --color-primary-dark: hsl(var(--hue), 60%, 40%);
}

This pattern (sometimes called "HSLu" for HSL with unbundled values) powers several popular component libraries and design systems.

Convert Between Formats

The Color Converter converts between HEX, RGB, and HSL in real time. Enter a HEX code and see the RGB and HSL equivalents. Adjust the HSL sliders and the HEX and RGB update instantly. This is the fastest way to understand how the same color looks in each format.

Try it yourself: open the Color Converter. Enter #008080 in the HEX field. Observe the RGB (0, 128, 128) and HSL (180, 100%, 25%) values. Use the HSL lightness slider to increase lightness to 50%. The color becomes a brighter teal, and the HEX and RGB update automatically. Try adjusting the saturation slider to see how muted the color becomes. Then paste a color from your own project and explore how it breaks down across all three formats.

Related Reading