MakeMyPalette
← All posts
8 min read

Understanding hex, RGB, and HSL — when to use each

How hex, RGB, and HSL encode color, why HSL is better for programmatic manipulation, and where modern formats like oklch fit in.

Three ways to say the same color

#6366f1, rgb(99, 102, 241), and hsl(239, 84%, 67%) all describe the same indigo. The pixel your monitor lights up is identical regardless of which format you use in CSS. The difference is how each format encodes color — and that affects how easy it is to read, modify, and reason about programmatically.

Hex: the compact default

Hex notation is a base-16 encoding of the RGB channels. Each pair of characters represents a channel intensity from 00 (0) to FF (255):

#RRGGBB
#6366f1
 ↑↑ ↑↑ ↑↑
 R  G  B
 99 102 241

The three-digit shorthand (#63f) doubles each digit: #6633ff. It’s not arbitrary abbreviation — it only works when each channel can be represented by a repeated digit.

Eight-digit hex (#6366f180) adds an alpha channel as the last two digits, where 00 is fully transparent and ff is fully opaque. This is the opposite of rgba() notation, where alpha is 0–1, so keep that in mind when converting.

When to use hex: Design tools (Figma, Sketch, Photoshop) default to hex. If you’re copying a color from a design file into CSS, hex is what you’ll get. It’s also the most compact format for opaque colors — six characters vs. the ~18 characters of an rgb() call.

Limitations: Hex is unreadable at a glance. Quick: is #3b82f6 more blue or more green? You have to mentally decode 3b=59, 82=130, f6=246 to know it’s heavily blue. Hex also gives you no intuitive handle on brightness or saturation — you can’t “make it 20% lighter” by editing the hex string.

RGB: the machine format

RGB describes color as a mix of red, green, and blue light, with each channel as an integer from 0 to 255 (or 0%–100% in modern CSS). The value maps directly to how monitors produce color — three subpixels per pixel, one for each channel.

rgb(99, 102, 241)
    R    G    B

Modern CSS also accepts the space-separated syntax without commas: rgb(99 102 241). Alpha can be added with a slash: rgb(99 102 241 / 0.5). The older rgba() function is now an alias — rgb() handles alpha natively.

When to use RGB: When you need alpha transparency. rgba(0, 0, 0, 0.1) for overlay backgrounds is a common pattern that reads clearly. RGB is also the native format for Canvas API (getImageData returns RGBA byte arrays), WebGL, and most image processing libraries.

Limitations: Like hex, RGB gives you no intuitive control over perceptual qualities. Lightening a color means increasing all three channels, but by how much each? A red at rgb(200, 50, 50) lightened by adding 50 to each channel becomes rgb(250, 100, 100) — but that shifts the hue toward pink. Saturation, brightness, and hue are entangled in the RGB model.

HSL: the human format

HSL separates color into three perceptually meaningful axes:

  • Hue (H): The base color, expressed as an angle from 0° to 360° on the color wheel. 0°=red, 120°=green, 240°=blue.
  • Saturation (S): How vivid the color is, from 0% (gray) to 100% (full color).
  • Lightness (L): How bright the color is, from 0% (black) through 50% (pure color) to 100% (white).
hsl(239, 84%, 67%)
     H    S    L

This is the same indigo as before: hue 239° (blue-violet), 84% saturation (vivid), 67% lightness (moderately bright).

When to use HSL: Whenever you need to manipulate color programmatically. HSL makes common operations trivial:

  • Lighten: increase L. hsl(239, 84%, 67%)hsl(239, 84%, 85%)
  • Darken: decrease L. hsl(239, 84%, 67%)hsl(239, 84%, 40%)
  • Desaturate: decrease S. hsl(239, 84%, 67%)hsl(239, 30%, 67%)
  • Get the complement: rotate H by 180°. hsl(239, 84%, 67%)hsl(59, 84%, 67%)
  • Build a monochromatic scale: keep H and S, vary L from 10% to 95%

None of these operations are straightforward in RGB or hex. HSL is the reason color palette generators work — the Color Palette Generator uses HSL hue rotation for every harmony type.

Limitations: HSL lightness is not perceptually uniform. An HSL lightness of 50% looks different depending on the hue: hsl(60, 100%, 50%) (yellow) appears much brighter to the eye than hsl(240, 100%, 50%) (blue), even though they have the same L value. This is because HSL is a mathematical transformation of RGB, not a perceptual model. For applications that need perceptual uniformity, newer formats are better.

Beyond sRGB: oklch and CSS relative colors

CSS Color Level 4 introduced several perceptually uniform color spaces. The most practical for web developers is oklch:

oklch(67% 0.2 260)
      L   C   H
  • L — perceptual lightness (0%–100%), calibrated so that equal L values look equally bright regardless of hue
  • C — chroma (saturation), unbounded but typically 0–0.4 for sRGB-displayable colors
  • H — hue angle (0°–360°), similar to HSL but perceptually spaced

The key improvement over HSL: lightness actually corresponds to perceived brightness. oklch(60% 0.2 60) (orange) and oklch(60% 0.2 260) (blue) genuinely look the same brightness. This matters when generating palette scales or ensuring consistent visual weight across different hue families.

Browser support for oklch in CSS is solid: Chrome 111+, Safari 15.4+, Firefox 113+. You can use it in production today with a fallback:

.button {
  background: hsl(239, 84%, 67%);
  background: oklch(67% 0.2 264);
}

CSS relative color syntax

The color-mix() function and the relative color syntax (RCS) let you derive colors in CSS without JavaScript:

/* Mix two colors */
color-mix(in oklch, #6366f1 70%, white)

/* Relative color: lighten by 20% */
hsl(from var(--brand) h s calc(l + 20%))

/* Relative color in oklch: desaturate */
oklch(from var(--brand) l calc(c - 0.05) h)

Relative color syntax is supported in Chrome 119+ and Safari 18+. It removes the need for preprocessor functions like Sass lighten() and darken() — the browser handles the color math at render time.

Choosing a format: a decision guide

There’s no single “best” format. The right choice depends on context:

  • Design handoff (static colors from Figma): use hex. It’s what the designer gave you and it’s compact.
  • Transparency: use rgb() with alpha. rgb(0 0 0 / 0.1) reads clearly as “10% black.”
  • Programmatic manipulation (palette generation, theming, lightening/darkening): use hsl(). Adjust channels independently without side effects.
  • Perceptual uniformity (consistent visual brightness across hues): use oklch(). Generate scales that look even to the human eye.
  • Canvas/WebGL: use RGB arrays. That’s the native pixel format.

In practice, most codebases use hex for static values and HSL (or oklch) for computed values. The Color Format Converter shows all formats side by side — paste any color and see its hex, RGB, RGBA, HSL, HSLA, and HSV representations instantly.

Converting between formats

Conversion between hex, RGB, and HSL is lossless — they all describe the same sRGB color space. Every hex value has an exact RGB equivalent (just decode the base-16) and an exact HSL equivalent (apply the geometric conversion).

The HSL ↔ RGB conversion uses min/max/chroma decomposition. Converting RGB to HSL: find the min and max channel values, compute lightness as their average, compute saturation from the delta relative to lightness, and compute hue from which channel was dominant. The inverse goes from H, S, L back to the sector of the color wheel and interpolates the channel values.

The math is well-defined and deterministic — there’s no approximation or quality loss. The only caveat is floating-point rounding: hsl(239.4, 83.7%, 66.7%) converted to hex and back might shift by a fraction of a degree. For practical purposes, this is invisible.

Ready to put this into practice?

Open the tool →