Hex vs. RGB vs. HSL: Which Model, When
Three notations for the same underlying value, each optimized for a different job — a practical decision guide, not just a syntax comparison.
#FF6347, rgb(255, 99, 71), and hsl(9, 100%, 64%) describe the exact same color — tomato red. None of them is objectively "better"; each is a different projection of the same additive RGB cube, optimized for a different kind of task. Picking the wrong one for the job you're doing is the actual mistake, not picking hex over HSL in the abstract.
Hex: compact, unambiguous, and what ships in most stylesheets
Hex packs the three RGB channels into a six-digit base-16 string, two digits per channel: FF (255), 63 (99), 47 (71). Its whole appeal is density — it's the shortest common notation, it copy-pastes cleanly between design tools and code, and it has zero ambiguity about units (no percent-vs-integer confusion the way RGB notation sometimes has across different tools). The tradeoff: it's opaque to read. Nobody looks at #FF6347 and immediately knows it's a warm, highly saturated, medium-light red without mentally converting it first. If you need to go from a hex string to any other representation, the hex-to-RGB converter and the full color converter do it instantly rather than by hand.
RGB: what the screen actually stores
RGB (0–255 per channel, or occasionally 0–1 floating point) is the closest notation to what a screen's framebuffer actually holds — each pixel is genuinely three separately-addressable light-emitting subpixels, and RGB numbers map onto that hardware reality almost directly (after gamma correction, which is a separate concern — see WCAG contrast explained for where linearization matters). This makes RGB the natural format for anything doing pixel-level image work: the image color extractor clusters raw RGB pixel values when pulling a palette out of a photo, because that's the native representation of the image data before any color-space conversion happens. If you're starting from individual channel values rather than a hex string, RGB-to-hex converts the other direction.
HSL: the one humans actually reason in
HSL re-parameterizes the identical RGB cube into hue (0–360°, a position on the color wheel), saturation (0–100%, how far from gray), and lightness (0–100%, how far from black or white). This is the notation that matches how people actually describe color verbally — "a slightly muted orange, on the darker side" maps almost directly onto HSL numbers, where the equivalent statement in RGB or hex requires doing the mental conversion first. That's why nearly every design tool's color picker defaults to an HSL or the closely related HSB/HSV wheel-plus-slider interface rather than three RGB sliders — dragging a hue wheel and a lightness slider is a far more intuitive way to *find* a color than typing RGB triples, even though the underlying stored value ends up as RGB either way. The color picker here exposes both representations side by side so the relationship is visible rather than hidden behind a single UI paradigm.
HSL is also the natural notation for generating related colors programmatically — rotate hue by 120° and hold saturation/lightness constant to get a triadic harmony; walk lightness in steps to get a tint/shade ramp. The catch, covered in more depth in what OKLCH is and why it matters, is that HSL's lightness channel doesn't track actual perceived brightness consistently across hues — a blue and a yellow at identical HSL lightness can look meaningfully different in brightness, because HSL lightness is a simple average of the RGB max and min rather than a model of human luminance perception. For most everyday harmony and ramp generation this is a minor wrinkle; for production design-token scales where visual evenness matters, OKLCH is the more accurate tool, and the shades, tints & tones generator and color harmonies tool on this site compute both so you can compare directly.
A practical decision table
- Writing a stylesheet by hand, want the shortest string → hex.
- Doing pixel-level image processing or reading raw framebuffer data → RGB.
- Picking, describing, or programmatically generating a color relationship (lighter/darker, more/less saturated, rotate hue) → HSL, or OKLCH if evenness across hues matters.
- Prepping a color for print → neither — convert to CMYK, and read print color: CMYK vs. screen RGB first, because the RGB-to-CMYK conversion is lossy and gamut-dependent in ways that catch people off guard.
- Building a design-token scale that needs to look evenly-stepped → OKLCH; see what OKLCH is and why it matters and building a design token color system.
They're all lossless conversions of each other, within a color space
One thing worth being explicit about: converting between hex, RGB, and HSL is mathematically lossless within the sRGB color space — you're not losing information going from one to another, just re-expressing the identical value (allowing for rounding at low decimal precision). This is different from, say, converting sRGB to CMYK, which genuinely can lose information because the two color spaces don't cover identical ranges of representable colors — see understanding color gamuts for that distinction. Round-tripping hex → HSL → hex with the color converter should return you to (very close to) the exact value you started with; that's a useful sanity check if you're ever unsure whether a conversion tool is doing real math or approximating.
In practice, most teams end up using all three
A realistic project uses hex in the stylesheet for compactness, HSL (or OKLCH) in the design tool and any programmatic palette generation for its intuitive controls, and occasionally raw RGB when touching image data directly. There's no reason to pick one notation as a house style for every context — the notation is just a lens on the same underlying value, and the right one depends on what you're doing with it at that moment, not on a general preference.
A quick note on alpha and shorthand variants
Each of the three main notations has an alpha-channel extension worth knowing: 8-digit hex (#FF6347CC, where the last two digits are alpha in hex), rgba(255, 99, 71, 0.8), and hsla(9, 100%, 64%, 0.8). All three are functionally interchangeable with their opaque counterparts once you strip the alpha component, and modern CSS has actually folded rgba()/hsla() into rgb()/hsl() directly — an alpha value can be passed as a fourth, slash-separated argument to the non-"a" function names (rgb(255 99 71 / 0.8)), which is now the preferred syntax in current CSS specs even though the older rgba()/hsla() names remain supported for backward compatibility and aren't going away. Three-digit hex shorthand (#F63 expanding to #FF6633) is a genuinely different, coarser value than its six-digit look-alike unless each digit happens to repeat itself, so treat shorthand hex as its own explicit value rather than assuming it's simply a "compressed" version of a specific six-digit color you had in mind — always expand it and verify before assuming equivalence.
Where OKLCH fits into this comparison
It's worth being explicit that OKLCH isn't a fourth entry competing on equal footing with hex/RGB/HSL for everyday use — it's a newer, more perceptually accurate cylindrical model solving the specific evenness problem HSL has, and it's covered on its own in what OKLCH is and why it matters rather than folded into this comparison, because the decision to reach for it is driven by a narrower set of use cases (ramps, gradients, programmatic generation) than the everyday hex/RGB/HSL choice this post focuses on.