Back to Blog
Design Tools6 min read·September 11, 2026

Color Converter Guide: HEX, RGB, HSL Explained for Designers

Master color formats used in web design. Learn how HEX, RGB, HSL, and HSB work, how to convert between them, and use our free color converter tool.

One Color, Three Coordinate Systems

HEX, RGB and HSL are not three kinds of color. They are three ways of writing a point inside the same box.

That box is sRGB, the color space almost every screen and CSS declaration assumes by default. A color in sRGB is three numbers: how much red light, green light, blue light. HEX writes them in base 16, RGB in base 10, and HSL reshapes the box into a cylinder so the numbers mean something to a human eye.

HEX and RGB are interchangeable with no loss — the same integers in two notations. HSL reaches the same point through a trigonometric reshaping, so a *rounded* HSL string is a near neighbour, not a twin. #3B82F6 is exactly rgb(59 130 246); its true HSL is hsl(217.22 91.22% 59.8%), and the tidy hsl(217 91% 60%) everyone quotes renders rgb(60 131 246) — one step off on every channel. Invisible on a button, but worth knowing before you assume a round trip through a Color Converter returns the byte you started with.

HEX: Three Bytes, Written in Base 16

A hex color is one byte per channel, two hex digits each:

.button { background: #3B82F6; }   /* 3B=59, 82=130, F6=246 */

Each pair runs 00 to FF — 0 to 255, 256 steps per channel, 16,777,216 combinations. Case is irrelevant: #3b82f6 and #3B82F6 parse identically.

Four forms are valid in CSS:

  • #RGB — shorthand. Each digit doubles, so #F0C expands to #FF00CC. It reaches only 4,096 colors, those whose channel pairs repeat.
  • #RGBA — shorthand with alpha: #F0C8 means #FF00CC88.
  • #RRGGBB — the standard form.
  • #RRGGBBAA — with an alpha byte. #3B82F6CC is that blue at CC = 204/255 = 80%.

The eight-digit form is the one people get wrong. 80 is not 50% alpha: 0x80 is 128, so 128/255 ≈ 50.2%, where a true half of 255 is 127.5. Close enough visually, but to hit a spec that says "40% opacity", compute round(0.40 × 255) = 102 = 0x66 — or write rgb(59 130 246 / 40%), which needs no decoding.

Hex cannot express a color outside sRGB, nor a hue directly. It is a serialization format, not a thinking format.

RGB: The Same Bytes in Decimal

rgb() holds the identical values in decimal:

.button {

background: rgb(59 130 246);

border-color: rgb(59 130 246 / 0.8); /* alpha after slash */

}

CSS Color Level 4 added the space-separated form, moved alpha behind a slash, and made rgba() a plain alias of rgb(). The comma form is still legal, so both work everywhere — but author the modern one. Reading values back is a different matter: getComputedStyle() still serializes the legacy comma form — rgb(59, 130, 246), or rgba(59, 130, 246, 0.8) once alpha drops below 1 — so any regex parsing computed styles must accept commas whatever you wrote.

Percentages are legal too (rgb(23% 51% 96%), where 100% means 255), and out-of-range values clamp rather than wrap, so rgb(300 -20 0) becomes rgb(255 0 0).

RGB is the format JavaScript speaks. Canvas getImageData() returns a flat Uint8ClampedArray of 0–255 values in RGBA order, so RGB is where you land sampling a photo with the Image Color Picker. The GPU is the exception: shaders want each channel as a 0.0–1.0 float, so a raw 0–255 triple in a vec3 uniform reliably produces blown-out white.

HSL: The Cylinder Humans Can Reason About

HSL takes the same cube and reshapes it into a cylinder, written hsl(217 91% 60%):

  • Hue — an angle, 0–360deg around the color wheel. 0 red, 60 yellow, 120 green, 180 cyan, 240 blue, 300 magenta. It wraps, so hsl(380 …) equals hsl(20 …).
  • Saturation — 0% is gray, 100% the most vivid version of that hue available at that lightness.
  • Lightness — 0% is always black, 100% always white, and 50% at full saturation is the pure hue.

The practical win is changing one property without disturbing the others. Same blue, lighter? Raise L. Muted for a disabled state? Lower S. In HEX, all three channels move.

HSB (also called HSV) looks similar and is not the same thing. It is what Figma, Sketch and Photoshop show, and its third axis is Brightness: 0% is black, but 100% is the *full-strength hue*, not white. Pure red is hsl(0 100% 50%) and hsb(0, 100%, 100%). Pasting HSB numbers into hsl() is a common, very visible bug — colors wash out, because Brightness 100% became lightness 100%, which is white. CSS has no hsb(), so convert first.

Converting HEX to RGB by Hand

Split into pairs and read each as base 16: the left digit is worth 16, the right 1 (A=10 … F=15). Take #2E8B57:

2E → 2×16 + 14 =  46

8B → 8×16 + 11 = 139

57 → 5×16 + 7 = 87

In code, expand shorthand first, then slice bytes from one integer:

function hexToRgb(hex) {

let h = hex.replace('#', '');

if (h.length === 3 || h.length === 4) {

h = [...h].map(c => c + c).join(''); // f0c → ff00cc

}

const int = parseInt(h.slice(0, 6), 16);

const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;

return { r: (int >> 16) & 255, g: (int >> 8) & 255, b: int & 255, a };

}

Going back is padding, not math: n.toString(16).padStart(2, '0') per channel. Skip the pad and rgb(5 40 97) serializes as #52861 — five characters, which no parser takes.

Converting RGB to HSL: The Actual Algorithm

Normalize each channel to 0–1, then take the largest and smallest. Continuing with rgb(46 139 87):

r = 0.1804 (min)   g = 0.5451 (max)   b = 0.3412

delta = max − min = 0.3647

L = (max + min)/2 = 0.3627 → 36.3%

S = delta / (1 −

2L − 1
) = 0.5027 → 50.3%

H = 60 × (2 + (b − r)/delta) → 146.5deg [green branch]

The largest channel picks the hue branch:

function rgbToHsl(r, g, b) {

r /= 255; g /= 255; b /= 255;

const max = Math.max(r, g, b), min = Math.min(r, g, b);

const d = max - min;

const l = (max + min) / 2;

if (d === 0) return { h: 0, s: 0, l: l * 100 };

const s = d / (1 - Math.abs(2 * l - 1));

let h;

if (max === r) h = 60 * (((g - b) / d) % 6);

else if (max === g) h = 60 * ((b - r) / d + 2);

else h = 60 * ((r - g) / d + 4);

if (h < 0) h += 360;

return { h, s: s * 100, l: l * 100 };

}

Two traps. When delta is 0 the color is gray and hue is undefined — return early instead of dividing by zero. The red branch also goes negative for hues just below 360, so add 360 before returning.

Rounding is the third trap, and it decides whether a conversion survives the round trip. Keep one decimal and hsl(146.5 50.3% 36.3%) converts back to rgb(46 139 87), the bytes you started with. Round to integers and hsl(146 50% 36%) gives rgb(46 138 86) — a different green. Keep HEX or RGB as the source of truth, treat HSL as a working view, round only for display.

Which Format for Which Job

FormatStrengthWeaknessUse it for

HEXCompact, universalNo hue axisBrand tokens, handoff
RGBNative to JS pixel APIsBrightness edits touch 3 numbersImage processing
HSLOne axis at a timeLightness is not perceptualTints, shades, states
HSB/HSVMatches design-tool pickersNot valid CSSReading Figma
OKLCHPerceptually even lightnessCan exceed sRGBEvenly spaced palettes

Building a Palette in HSL — and Where HSL Lies

The standard move is to fix the hue and walk the lightness:

:root {

--blue-100: hsl(217 91% 95%);

--blue-500: hsl(217 91% 60%);

--blue-900: hsl(217 91% 20%);

}

That works, and relative color syntax — supported in current Chrome, Safari and Firefox — derives the ramp from one token instead of hardcoding each step:

.card:hover {

background: hsl(from var(--brand) h s calc(l - 8%));

}

Here is the lie. HSL lightness is a geometric midpoint, not a measure of how bright a color looks. hsl(60 100% 50%) (yellow) and hsl(240 100% 50%) (blue) both claim 50% lightness. Their WCAG relative luminance — each channel linearized out of sRGB's transfer curve, then weighted 0.2126R + 0.7152G + 0.0722B — is about 0.93 for the yellow and 0.07 for the blue. One needs black text, the other white.

So an HSL ramp holds together within one hue and falls apart the moment blue-500, green-500 and yellow-500 are expected to read as equally heavy. Either check every pair in the Color Contrast Checker against WCAG's 4.5:1 threshold for body text and 3:1 for large text and UI components, or build the ramp in OKLCH, where equal L does mean equal perceived lightness.

For a starting ramp rather than hand-tuning, the Color Shades Generator produces tints and shades from a base color, and the Color Palette Generator builds schemes to convert into whatever notation your stylesheet wants.

#color converter#hex to rgb#hsl color#web design#css colors

Try Our Free Online Tools

100+ free tools for developers, designers, and everyone. No sign-up required.

Browse All Tools