CSS gives you three ways to describe the same 16.7 million colors: HEX, RGB, and HSL. They are fully interchangeable as far as the browser is concerned — #FF6B6B, rgb(255, 107, 107), and hsl(0, 100%, 71%) all render the same coral-red. But each format has contexts where it is easier to read, easier to manipulate programmatically, or better suited to a particular workflow.

This guide covers the structure of all three formats, the precise conversion formulas between them, and practical advice for choosing the right format in your codebase. Each section includes working JavaScript code you can drop into a project directly.

HEX Colors: Compact and Familiar

Anatomy of a HEX Value

A CSS HEX color is a hash sign followed by six hexadecimal digits: #RRGGBB. Each pair represents a channel (Red, Green, Blue) as a number from 00 (0) to FF (255). The digits are case-insensitive — #ff6b6b and #FF6B6B are identical.

FormatExampleChannels
#RRGGBB#FF6B6BR=255, G=107, B=107
#RGB#F6BShorthand for #FF66BB
#RRGGBBAA#FF6B6B80+ Alpha=128 (50%)
#RGBA#F6B8Shorthand for #FF66BB88

The Three-Digit Shorthand

When each pair of hex digits is identical — #AA, #BB, #CC — you can collapse the pair to a single digit. #AABBCC becomes #ABC. This only works when both digits in each pair are the same. #FF6B6B cannot be shortened because 6B is not a doubled digit.

When HEX Falls Short

HEX is hard to interpret at a glance. #7B68EE is medium slate blue — not obvious from the digits. Modifying a HEX color requires converting it to RGB or HSL mentally, adjusting, then converting back. This is where programmatic tools or a color converter are most useful.

RGB Colors: The Machine's Language

The Three-Channel Model

rgb(red, green, blue) expresses each channel as an integer from 0 to 255 or as a percentage from 0% to 100%. The modern CSS rgb() syntax (CSS Color Level 4) accepts space-separated values without commas: rgb(255 107 107). Both syntaxes are valid in all modern browsers.

/* Legacy comma syntax */
color: rgb(255, 107, 107);

/* Modern space syntax */
color: rgb(255 107 107);

/* Using percentages */
color: rgb(100% 42% 42%);

RGBA: Adding Transparency

rgba() adds a fourth parameter — alpha — from 0 (fully transparent) to 1 (fully opaque). The modern rgb() function accepts alpha inline using the slash syntax: rgb(255 107 107 / 50%).

/* Legacy */
color: rgba(255, 107, 107, 0.5);

/* Modern (preferred) */
color: rgb(255 107 107 / 50%);
color: rgb(255 107 107 / 0.5);

RGB for Programmatic Manipulation

RGB shines when you need to compute colors in JavaScript. Canvas API, WebGL, and image processing all operate in RGB. Blending two colors, adjusting brightness by a fixed amount, or building a color gradient from code is straightforward in RGB arithmetic.

// Lighten a color by adding a fixed amount to each channel
function lighten(r, g, b, amount) {
    return [
        Math.min(255, r + amount),
        Math.min(255, g + amount),
        Math.min(255, b + amount)
    ];
}

lighten(255, 107, 107, 30)  // [255, 137, 137]

HSL Colors: The Designer's Language

Hue, Saturation, Lightness

HSL describes colors the way humans think about them. Hue is a degree on the color wheel (0–360): 0° is red, 120° is green, 240° is blue, 360° wraps back to red. Saturation is the intensity of the color, from 0% (gray) to 100% (fully saturated). Lightness is the brightness, from 0% (black) to 100% (white), with 50% being a "normal" color.

hsl(0, 100%, 71%)    /* coral red */
hsl(120, 60%, 50%)   /* mid green */
hsl(240, 70%, 60%)   /* periwinkle blue */
hsl(0, 0%, 50%)      /* medium gray — saturation 0 */

Why Designers Prefer HSL

Creating a color scale — lighter and darker versions of the same hue — is trivial in HSL: change only the lightness value. To make a color 10% lighter, add 10 to the L value. To desaturate it, reduce S. In RGB or HEX you would need to re-derive all three channels.

/* A button in three states */
--btn-normal:  hsl(215, 80%, 55%);
--btn-hover:   hsl(215, 80%, 50%);   /* 5% darker */
--btn-active:  hsl(215, 80%, 45%);   /* 10% darker */
--btn-muted:   hsl(215, 30%, 55%);   /* desaturated */

HSLA and Modern Syntax

Like RGBA, hsla() adds alpha. The modern syntax uses slash notation: hsl(215 80% 55% / 0.8).

Conversion Formulas

HEX to RGB

Parse each two-digit hex pair with parseInt(hex, 16).

function hexToRgb(hex) {
    const clean = hex.replace("#", "");
    const full = clean.length === 3
        ? clean.split("").map(c => c + c).join("")
        : clean;
    return {
        r: parseInt(full.slice(0, 2), 16),
        g: parseInt(full.slice(2, 4), 16),
        b: parseInt(full.slice(4, 6), 16),
    };
}

hexToRgb("#FF6B6B")  // { r: 255, g: 107, b: 107 }
hexToRgb("#F6B")     // { r: 255, g: 102, b: 187 }

RGB to HEX

Convert each channel to hex with toString(16) and pad to two digits.

function rgbToHex(r, g, b) {
    return "#" + [r, g, b]
        .map(v => v.toString(16).padStart(2, "0"))
        .join("")
        .toUpperCase();
}

rgbToHex(255, 107, 107)  // "#FF6B6B"

RGB to HSL

This is the most involved conversion. Normalize each channel to [0, 1], find the min and max, derive H, S, and L.

function rgbToHsl(r, g, b) {
    r /= 255; g /= 255; b /= 255;
    const max = Math.max(r, g, b);
    const min = Math.min(r, g, b);
    const delta = max - min;
    let h = 0, s = 0;
    const l = (max + min) / 2;

    if (delta !== 0) {
        s = delta / (1 - Math.abs(2 * l - 1));
        switch (max) {
            case r: h = ((g - b) / delta) % 6; break;
            case g: h = (b - r) / delta + 2; break;
            case b: h = (r - g) / delta + 4; break;
        }
        h = Math.round(h * 60);
        if (h < 0) h += 360;
    }

    return { h, s: Math.round(s * 100), l: Math.round(l * 100) };
}

rgbToHsl(255, 107, 107)  // { h: 0, s: 100, l: 71 }

HSL to RGB

Use the intermediate chroma calculation.

function hslToRgb(h, s, l) {
    s /= 100; l /= 100;
    const c = (1 - Math.abs(2 * l - 1)) * s;
    const x = c * (1 - Math.abs((h / 60) % 2 - 1));
    const m = l - c / 2;
    let [r, g, b] = [0, 0, 0];

    if      (h < 60)  { r = c; g = x; b = 0; }
    else if (h < 120) { r = x; g = c; b = 0; }
    else if (h < 180) { r = 0; g = c; b = x; }
    else if (h < 240) { r = 0; g = x; b = c; }
    else if (h < 300) { r = x; g = 0; b = c; }
    else              { r = c; g = 0; b = x; }

    return {
        r: Math.round((r + m) * 255),
        g: Math.round((g + m) * 255),
        b: Math.round((b + m) * 255),
    };
}

hslToRgb(0, 100, 71)  // { r: 255, g: 107, b: 107 }

Edge Cases

Pure Grays: Saturation Is Undefined

When r === g === b, the color is a shade of gray. In HSL, the hue and saturation are mathematically undefined (delta = 0). Implementations typically set H = 0 and S = 0 for gray. This means the hue information is lost when you round-trip a gray through RGB → HSL → RGB: any hue value is equally valid.

rgbToHsl(128, 128, 128)  // { h: 0, s: 0, l: 50 }
// All grays have s=0, and h is arbitrary

Black and White

Black is hsl(0, 0%, 0%) — the hue and saturation are irrelevant since lightness is 0. White is hsl(0, 0%, 100%). When manipulating lightness programmatically, clamp the output to [0%, 100%] to avoid producing values that are technically valid syntax but meaningless.

When to Use Each Format

Use HEX for Design Tokens and Version Control

HEX is compact and universally understood by design tools (Figma, Sketch, Adobe) and CSS preprocessors. Design token files and theme variables are clearest in HEX. Changes are minimal in git diffs: #1a73e8#1557b0 is immediately clear.

Use RGB for Canvas, WebGL, and Image Processing

Canvas's fillStyle accepts HEX strings, but computations require RGB integers. Any time you need to blend, tint, or decompose a color mathematically, work in RGB.

Use HSL for Theme Systems and Accessible Color Scales

Building a full color system — multiple shades of each brand color, accessible variants with sufficient contrast — is most maintainable in HSL. Lightness adjustments are linear and predictable. HSL also makes it easy to verify that two colors share the same hue family.

To convert the value instantly between all three formats without writing code, an online color converter gives you HEX, RGB, and HSL side by side with a live preview.

Beyond sRGB — Modern CSS Color Spaces

Modern CSS (Color Level 4 and 5) introduces oklch(), oklab(), display-p3, and others. These cover a wider gamut than sRGB and are perceptually uniform (equal steps in L produce equal perceived brightness). For now, HEX/RGB/HSL remain the safe default for cross-browser compatibility, but oklch is gaining traction for accessible color palette generation.

Handling color format mismatches — where a JSON theme file or API returns HEX but your CSS expects HSL — is a common source of common JSON errors during design-to-code handoff. Defining the canonical format in your design tokens and converting at the boundary is the cleanest solution.