Hex vs RGB vs HSL: Choosing the Right CSS Color Format
CSS has represented color the same three main ways for a long time: hex, rgb(), and
hsl(). They're mathematically interchangeable — the same color always converts cleanly between
them — but they are not equally easy to work with. Picking the right one for a given task means fewer
"why is this color slightly off" moments and less trial-and-error in devtools.
Hex: compact, copy-pasteable, opaque to edit
Hex packs red, green, and blue into two-digit hexadecimal pairs: #RRGGBB, each pair running
from 00 to ff (0–255 in decimal). A shorthand exists when each pair repeats its own
digit — #0af expands to #00aaff. Modern CSS also supports an 8-digit form with an
alpha pair on the end, #RRGGBBAA.
.brand { color: #7c3aed; } /* opaque */
.overlay { background: #201b39cc; } /* ~80% opacity, cc = 204/255 */
Hex is the format design tools export by default, which makes it the easiest to paste in directly from a
Figma or Sketch spec. Its weakness shows up the moment you need to change the color — there's no
obvious way to look at #7c3aed and know how to make it 10% darker without a calculator.
RGB: the same numbers, more legible math
rgb() spells out the same red/green/blue values in decimal (0–255), and modern CSS's
space-separated syntax adds the alpha channel with a slash:
.brand { color: rgb(124 58 237); }
.overlay { background: rgb(32 27 57 / 0.8); }
(The older comma-separated rgb(124, 58, 237) and rgba(32, 27, 57, 0.8) forms
still work in every browser and are functionally identical — the space syntax is just the current
recommendation.) RGB is marginally easier to reason about than hex because the numbers are decimal, but it
shares hex's core problem: there's no channel here that maps to "how light or saturated does this look,"
so adjusting a color by feel still means guessing at three numbers at once.
HSL: the format built for tweaking
hsl() describes a color by hue (0–360°, a position on the color wheel),
saturation (0–100%, how vivid vs. gray), and lightness (0–100%, how close
to black or white):
.brand { color: hsl(262 83% 58%); }
.brand-hover{ color: hsl(262 83% 48%); } /* same hue/sat, 10 points darker */
This is the format that actually matches how people describe color changes in a design review — "a bit darker," "less saturated," "shift it slightly more blue." Each of those is a one-number edit in HSL: lightness down for darker, saturation down for muted, hue up or down a few degrees for a warmer or cooler shift. Building a full color scale (a lighter tint and a darker shade of one brand hue for hover, active, and disabled states) is usually a handful of lightness steps on the same hue/saturation pair, rather than three unrelated hex codes picked by eye.
Comparison at a glance
| Format | Best for | Weak point |
|---|---|---|
| Hex | Pasting values straight from a design tool | Hard to tweak by feel |
| rgb() | Working with color extracted from images/data (0–255 channels) | Still non-intuitive to adjust |
| hsl() | Building hover/active/disabled variants, theming, tints and shades | Less common in exported design specs |
Newer CSS color tools worth knowing
Recent CSS adds a few more powerful moves on top of these three formats. color-mix() blends
two colors by a percentage without manual math: color-mix(in srgb, #7c3aed 80%, white) gives an
80%-brand, 20%-white tint in one line. Relative color syntax lets you derive a variant directly from an
existing color, e.g. hsl(from var(--brand) h s calc(l - 10%)) for "the brand color, 10 points
darker," which keeps a single source of truth instead of hand-picking a second hex value that might drift
out of sync with the first.
Moving between formats without losing precision
Because all three describe the same underlying color space, converting between them is lossless for the values browsers actually render — the hex, RGB & HSL converter does the conversion both ways and shows a live swatch, which is the fastest way to confirm "is this hex code actually the blue I think it is" before it ships. Small rounding can appear if you convert back and forth by hand through intermediate decimal steps — the tool avoids that by computing straight from the original input each time rather than chaining conversions.
Color format is not an accessibility strategy
Whichever format you write a color in, the number that actually matters for text legibility is the contrast ratio between it and its background — not the format. A color that "looks" fine can still fail WCAG's 4.5:1 threshold for normal text. See the WCAG contrast guide and the contrast ratio checker before shipping a color pairing.
Alpha channels beyond simple transparency
An alpha value is often reached for as "make it see-through," but it's also the more maintainable way to
build a scrim or overlay that needs to work over unpredictable content. A fixed dark background color over a
photo hides whatever's underneath completely; a semi-transparent overlay like
rgb(0 0 0 / 0.55) darkens the photo enough for white text to stay legible while still letting
the image itself show through. The same idea covers a focus ring drawn as a translucent halo, or a disabled
button that's the same hue as its enabled state but at a reduced opacity rather than a separate, hand-picked
gray.
Why design tools export hex by default
Figma, Sketch, and most other design tools default to hex specifically because it's the most copy-paste-friendly, unambiguous representation — a single string with no unit or function syntax to get wrong. That's a reasonable default for handing a spec to a developer, but it's worth translating a design system's core palette into HSL (or generating HSL-based tints/shades from it) once, early, rather than hand-picking a separate hex value for every hover, active, and disabled state a component needs — the HSL-derived set stays visibly related to the base color in a way that several independently-chosen hex codes usually don't.
A quick mental-math cheat sheet
| Hex pair | Decimal | ~ % of 255 |
|---|---|---|
00 | 0 | 0% |
40 | 64 | 25% |
80 | 128 | 50% |
bf | 191 | 75% |
ff | 255 | 100% |
Knowing these five reference points makes it possible to eyeball roughly how opaque an 8-digit hex alpha value is, or roughly how bright a channel is, without reaching for a calculator every time.
Frequently Asked Questions
Is one format faster for the browser to render?
No — hex, rgb(), and hsl() are parsed to the same internal representation, so there's no performance difference between them.
Can I mix formats in the same stylesheet?
Yes, freely. Many teams use hex for values pasted from a design tool and hsl() for anything computed or adjusted in code — there's no requirement to standardize on one.
What does the alpha value in an 8-digit hex actually mean?
The last pair is an alpha channel from 00 (fully transparent) to ff (fully
opaque) — 80 is roughly 50%, since 128⁄255 ≈ 0.502.
Why does my HSL color look different from the hex I converted it from?
It shouldn't, if the conversion is exact — double-check you copied all three HSL numbers (including the % signs on saturation and lightness) and didn't round the hue.
What's the difference between HSL and the newer HWB or LCH formats?
HWB (hue, whiteness, blackness) is another intuitive, human-friendly format similar in spirit to HSL. LCH and OKLCH are perceptually uniform color spaces — a given numeric change in lightness looks like the same amount of visual change across every hue, which HSL doesn't quite guarantee. They're worth knowing as CSS support grows, but hex/rgb/hsl remain the formats every browser and design tool has supported for years.
Should I store colors as design tokens instead of raw values in CSS?
For anything beyond a one-off page, yes — a small set of named custom properties (--color-brand,
--color-ink, etc.) defined once and referenced everywhere keeps a palette change to a single
edit, regardless of which format the underlying value is written in.