CSS Color Formats (HEX, RGB, HSL)
Every color you see on a web page — text, backgrounds, borders, shadows — is described to the browser using one of a handful of CSS color notations. The three you will use constantly are HEX (hexadecimal), RGB/RGBA (red-green-blue, optionally with alpha), and HSL/HSLA (hue-saturation-lightness, optionally with alpha). They are not competing systems — they are three different ways of writing down the exact same underlying color, and the browser converts between them internally before it ever paints a pixel. Understanding all three, and when each is most convenient, will make you dramatically faster at building and adjusting color schemes.
Overview / How it works
Under the hood, a browser’s rendering engine ultimately represents every color as red, green, and blue light intensities (plus an alpha/opacity channel), because that is how screens produce color — by mixing red, green, and blue sub-pixels at varying brightness. Whichever notation you write in your stylesheet, the CSS parser converts it into this same internal RGBA representation before compositing. This is why HEX, RGB, and HSL are perfectly interchangeable: #1a73e8, rgb(26, 115, 232), and hsl(217, 89%, 51%) all describe the identical shade of blue.
HEX is a base-16 encoding of the red, green, and blue channels, each ranging from 00 to ff (0–255 in decimal). It is compact and extremely common in design tools, style guides, and brand documentation, but it is not very human-readable — you cannot look at #2e7d32 and immediately know it is a medium-dark green without practice.
RGB writes the same three channels as decimal numbers (0–255) or percentages, which is more explicit but still not intuitive for adjusting a color’s brightness or vividness — changing all three numbers proportionally to make a color “lighter” is fiddly.
HSL describes color the way humans actually think about it: hue (an angle on the color wheel, 0–360 degrees, where 0 is red, 120 is green, and 240 is blue), saturation (how vivid vs. gray the color is, 0%–100%), and lightness (how close to black or white it is, 0%–100%). This makes HSL by far the easiest format for programmatically generating palettes, hover states, and themes, because you can hold the hue constant and just tweak lightness or saturation.
All of these formats also support an alpha (transparency) channel — RGBA, HSLA, and 4- or 8-digit HEX — which controls how much of whatever is behind an element shows through. The browser computes this using alpha compositing: it blends the foreground color and the background color proportionally to the alpha value during paint, which is why semi-transparent colors are commonly used for overlays, shadows, and glassy UI effects.
Syntax
The general forms are:
color: #rrggbb;
color: #rrggbbaa;
color: rgb(r, g, b);
color: rgb(r g b / a);
color: hsl(h, s%, l%);
color: hsl(h s% l% / a);
| Format | Parts | Range |
|---|---|---|
#rrggbb |
red, green, blue in hex | 00–ff each |
#rrggbbaa |
adds an alpha channel in hex | 00 (transparent) – ff (opaque) |
rgb(r, g, b) |
red, green, blue | 0–255 or 0%–100% |
rgb(r g b / a) |
modern space-separated syntax with optional alpha | a is 0–1 or 0%–100% |
hsl(h, s%, l%) |
hue, saturation, lightness | h: 0–360; s, l: 0%–100% |
hsl(h s% l% / a) |
modern space-separated syntax with optional alpha | same as above plus alpha |
There are two accepted syntaxes for rgb() and hsl(): the classic comma-separated form (rgb(26, 115, 232)) and the newer space-separated form with a slash before alpha (rgb(26 115 232 / 0.5)). Both are valid in modern evergreen browsers; you should not mix commas and slashes within the same function call, as that is invalid syntax.
Examples
Example 1: The same color, three ways
.header {
color: #1a73e8;
}
.tagline {
color: rgb(26, 115, 232);
}
.subtitle {
color: hsl(217, 89%, 51%);
}
Result: All three pieces of text — the header, the tagline, and the subtitle — render in the exact same medium blue, because #1a73e8, rgb(26, 115, 232), and hsl(217, 89%, 51%) are three encodings of the identical color.
This is the core lesson: pick whichever format is most convenient for the situation. If a designer hands you a hex code from a design tool, use HEX. If you’re computing a color at runtime or want to reason about hue and lightness, use HSL.
Example 2: Transparency with RGBA and HSLA
.overlay {
background-color: rgba(0, 0, 0, 0.6);
color: #ffffff;
padding: 24px;
}
.overlay-alt {
background-color: hsla(0, 0%, 0%, 0.6);
}
Result: Applied to a box (for example a div sitting on top of a photo), the box gets a black background at 60% opacity, so the image or content behind it shows through faintly rather than being fully hidden, with white text sitting legibly on top. .overlay-alt produces the visually identical result using HSLA instead of RGBA — black is hsl(0, 0%, 0%) regardless of hue, since saturation is 0%.
This pattern — a translucent dark layer over an image with light text on top — is one of the most common uses of alpha color values in real interfaces (hero banners, modals, image captions).
Example 3: An HSL-based theme with a hover state
:root {
--brand-hue: 262;
--brand-color: hsl(var(--brand-hue), 83%, 58%);
--brand-color-dark: hsl(var(--brand-hue), 83%, 38%);
}
.card {
background-color: #ffffff;
border: 1px solid hsl(var(--brand-hue), 20%, 85%);
color: #1f2937;
}
.card__button {
background-color: var(--brand-color);
color: #ffffff;
}
.card__button:hover {
background-color: var(--brand-color-dark);
}
Result: The card renders with a white background and a very light purple-tinted border. Its button renders as a vivid purple. On hover, the button becomes a noticeably darker shade of the same purple, because only the lightness value changed (58% down to 38%) while the hue (262, a purple) and saturation (83%) stayed identical.
This is where HSL shines over HEX or RGB: by storing the hue in a single custom property and deriving lighter/darker/muted variants by changing only saturation or lightness, you can build an entire consistent color system without ever hand-picking multiple hex codes that need to visually relate to each other.
Under the hood: how the browser resolves these values
When the rendering engine encounters a color value during style computation, it goes through roughly these steps:
1. Parse the token
The CSS parser recognizes the value’s shape — a hash token (#), a function token (rgb(, hsl(), or a keyword (red, transparent, currentColor) — and validates it against the grammar for that color type (correct number of digits for hex, correct argument count and units for functions).
2. Convert to a common internal representation
HSL values are mathematically converted to RGB (there is a well-defined formula using the hue angle and saturation/lightness percentages), and HEX digits are converted from base-16 to base-10. After this step, every color — regardless of how you wrote it — is stored as red, green, blue, and alpha components.
3. Compute alpha compositing during paint
If an element’s color has an alpha less than 1, the engine blends it with whatever is painted behind it using the standard “over” compositing formula: result = (foreground × alpha) + (background × (1 - alpha)), applied per channel. This is why a 60%-opacity black overlay over a bright image still lets you make out shapes and colors underneath, and why stacking two semi-transparent boxes produces a different result depending on paint order.
4. Apply the cascade like any other property
Color values participate in the normal cascade and specificity rules just like any other property — a more specific selector, or one later in the stylesheet with equal specificity, wins, and !important overrides normal-weight declarations. There is nothing special about color values in this respect; the only extra step is the format conversion described above.
Common Mistakes
Mistake 1: Invalid HEX length
HEX colors must be exactly 3, 4, 6, or 8 digits (shorthand, shorthand+alpha, full, or full+alpha). Any other length is invalid and the whole declaration is dropped.
.broken {
color: #12345;
}
This is wrong because #12345 has 5 digits, which does not map to any valid hex color format. The browser ignores this declaration entirely, and the element falls back to whatever color it would otherwise inherit or default to — not to some “closest guess.”
.fixed {
color: #123456;
}
Padding it out to a full 6 digits (or reducing it to a valid 3-digit shorthand like #135 if that’s the intended color) makes it valid, and the text now renders in the intended dark navy color.
Mistake 2: Forgetting the percent sign in HSL
Unlike hue (a plain number of degrees), the saturation and lightness parts of hsl() must be percentages — a bare number without % is invalid.
.broken-hsl {
background-color: hsl(200, 50, 40%);
}
This is wrong because 50 (the saturation) is missing its % sign. The entire hsl() function is considered invalid as a result, so the browser drops the whole declaration rather than guessing you meant 50%.
.fixed-hsl {
background-color: hsl(200, 50%, 40%);
}
With the percent sign restored on both saturation and lightness, the declaration is valid and renders a muted teal-blue background.
Best Practices
- Use HEX for colors copied directly from design tools, brand guidelines, or style references, since that’s the format designers usually hand off.
- Use HSL when you need to programmatically derive related shades (hover states, disabled states, tints/shades) by adjusting only lightness or saturation while keeping hue fixed.
- Prefer the 8-digit HEX or the modern
rgb()/hsl()slash syntax for alpha over always reaching for the older comma-basedrgba()/hsla()functions — both still work, but the newer syntax is more consistent and lets any color function accept alpha the same way. - Never mix comma-separated and slash/space syntax within a single
rgb()orhsl()call — pick one style per function. - Store brand and theme colors as CSS custom properties (e.g.
--brand-color) rather than repeating literal color values across a stylesheet, so a single change updates every usage. - Check contrast, not just aesthetics — a color pairing that looks fine to you may fail accessibility contrast guidelines for text readability; verify with a contrast checker rather than eyeballing it.
- Remember that lowering alpha does not lighten a color the way increasing HSL lightness does — alpha blends with whatever is behind the element, while lightness changes the color itself regardless of background.
Practice Exercises
Exercise 1: Write three separate rules for a class called .brand-text — one using HEX, one using rgb(), and one using hsl() — that all produce the same shade of orange. (Hint: pick an HSL hue around 30, then convert it, or look up an RGB/HEX orange and work out its approximate HSL values.)
Exercise 2: Create a .badge rule with a solid background color, then add a .badge--muted variant using the same hue in HSL but with lower saturation and higher lightness, so it looks like a faded version of the original badge.
Exercise 3: Build a translucent tooltip box: give it a background using hsla() or the modern hsl(... / a) syntax with roughly 85% opacity, and explain in a comment (in your own notes, not in the CSS) what would visually change if you lowered the alpha to 0.3.
Summary
- HEX, RGB(A), and HSL(A) are three different notations for describing the exact same underlying color — the browser converts them all to a common RGBA representation before painting.
- HEX is compact and common in design handoffs; RGB is explicit about channel values; HSL is the easiest format for reasoning about and generating related shades.
- Alpha transparency is available in every format (4/8-digit HEX, RGBA, HSLA, or the modern slash syntax) and is resolved through alpha compositing with whatever is painted behind the element.
- Invalid hex lengths and missing percent signs on HSL saturation/lightness are common mistakes that cause the browser to silently drop the entire declaration.
- Color values follow the normal CSS cascade and specificity rules just like any other property.
- Using CSS custom properties to store hue values (for HSL) or base colors makes it far easier to build a consistent, maintainable color system.
