There is no single best way to write colors in CSS. Each format exists for a reason, and knowing when to use which one removes a category of micro-decisions from your workflow.
The basics: what are you actually writing?
Every CSS color format describes the same thing: a point in a color space, encoded as text the browser can parse. The difference is how that encoding works and what operations it makes easy.
HEX (#RRGGBB)
color: #2f855a;
background: #1a202c;
border-color: #e2e8f0;
Structure: Three pairs of hexadecimal digits. Each pair represents one color channel (red, green, blue) on a scale from 00 (0) to ff (255).
Why it exists: Compact and universally supported. A hex code is shorter than its RGB equivalent and trivially copy-pasteable from any design tool (Figma, Sketch, Adobe XD all export hex by default).
3-digit shorthand: #fff expands to #ffffff. Works only when each pair repeats the same digit: #123 = #112233.
8-digit hex (with alpha): Add two more digits for opacity.
/* 50% transparent */
background: #2f855a80;
/* 80% opaque */
background: #2f855acc;
00 = fully transparent, ff = fully opaque. Use rgba() for older tool compatibility.
When to use: Any time you have a color from a design file. When you want compact, copy-pasteable color values. When you need a universally readable format across teams.
Limitation: Not intuitive to manipulate programmatically. To lighten #2f855a by 10%, you need to do the math or convert to HSL first.
RGB and RGBA
color: rgb(47, 133, 90);
background: rgba(26, 32, 44, 0.9);
Structure: Three decimal numbers (0–255) for each channel, plus an optional fourth alpha value (0–1 as a decimal).
When to use:
When reading computed styles from JavaScript.
getComputedStyle()returns colors inrgb()format — matching that format simplifies comparison logic.When you need dynamic color assembly. Building color values from variables is readable in RGB:
const alpha = 0.8;
const r = 47, g = 133, b = 90;
element.style.background = `rgba(${r}, ${g}, ${b}, ${alpha})`;
Limitation: Hard to imagine what rgb(200, 100, 150) looks like without a color picker. Not intuitive for design work.
HSL (Hue, Saturation, Lightness)
color: hsl(145, 48%, 35%);
background: hsla(145, 48%, 35%, 0.15);
Structure: Hue is an angle (0–360°), saturation is a percentage, lightness is a percentage. hsl(145deg, 48%, 35%) works too — the deg unit is optional.
Color wheel positions: 0° = red, 60° = yellow, 120° = green, 180° = cyan, 240° = blue, 300° = magenta.
Why HSL is powerful for design systems:
:root {
--brand: hsl(145, 48%, 35%);
--brand-light: hsl(145, 48%, 90%); /* same hue, lighter */
--brand-muted: hsl(145, 20%, 35%); /* same hue, less vivid */
--brand-dark: hsl(145, 48%, 20%); /* same hue, darker */
--brand-hover: hsl(145, 48%, 28%); /* same hue, slightly darker for hover */
}
Without looking up a hex color, you know these are all related. The hue (145) stays constant; saturation and lightness shift. Try doing that with hex codes.
When to use: Building design systems where you need related color variants. Any time you want hover states, disabled states, or tint/shade relationships to be readable from the CSS itself.
Limitation: Perceptual uniformity is imperfect. Changing lightness by the same amount looks different at different hues — a yellow at L=50% looks brighter than a blue at L=50%. This is a real problem when building accessible color scales.
Modern CSS: oklch
color: oklch(55% 0.12 145);
Structure: Lightness (0–100%), Chroma (0–~0.4), Hue (0–360°). Looks similar to HSL but the math underneath is different.
Why oklch exists: HSL's perceptual uniformity problem. oklch is built on the Oklab color space, designed so that equal changes in L actually look equally different — a property called perceptual uniformity. This makes it dramatically better for programmatic color generation:
/* These all look roughly the same "brightness" despite having different hues */
.red { color: oklch(60% 0.2 0); }
.green { color: oklch(60% 0.2 120); }
.blue { color: oklch(60% 0.2 240); }
With HSL at the same lightness value, green looks noticeably brighter than blue or purple. oklch fixes this.
Browser support: Chrome 111+, Firefox 113+, Safari 15.4+. Growing fast. For new design tokens, it is worth serious consideration.
Comparing the same color in every format
Here is #2f855a in all formats:
| Format | Value |
|---|---|
| HEX | #2f855a |
| RGB | rgb(47, 133, 90) |
| HSL | hsl(145, 48%, 35%) |
| oklch | oklch(52% 0.12 154) |
They render identically. If you have a hex value and need to check HSL or oklch, an online color picker can convert between all formats instantly — useful for inspecting design tokens or adapting an existing palette to a new color model.
Quick decision guide
| Situation | Use |
|---|---|
| Color from Figma / Sketch / XD | HEX |
| Assembling colors in JavaScript | RGB |
| Design system tokens with variants | HSL |
| New design system, modern browsers | oklch |
| Transparency |
rgba() or 8-digit HEX |
| Print design (CSS is wrong here, use design tools) | CMYK (not CSS) |
Converting between formats
You rarely need to do this by hand. Options:
Browser DevTools: Click any color swatch in the Styles panel and press Shift to cycle between
hex,rgb, andhsl— fastest method when inspecting existing styles.Online converter: Paste a hex code into a color picker to see RGB, HSL, and CMYK equivalents instantly.
In CSS: The
color-mix()function lets you mix colors across spaces without manual conversion:
color: color-mix(in oklch, hsl(145, 48%, 35%), white 20%);
- In code (JavaScript):
// Hex to RGB
function hexToRgb(hex) {
return {
r: parseInt(hex.slice(1, 3), 16),
g: parseInt(hex.slice(3, 5), 16),
b: parseInt(hex.slice(5, 7), 16)
};
}
Understanding which format to use and why eliminates a recurring decision from your workflow. Format consistency within a project matters more than which format you pick — choose one and stick to it for your design tokens.
Top comments (0)