Hex to HSL: when HSL helps and the algorithm in plain words
HSL is the same colour as hex but rotated into a coordinate system designed for humans. The reason to convert is almost always theming. Once you have HSL you can derive hover, disabled, and accessible variants of a brand colour with arithmetic instead of guesswork.
Why HSL pays off
Hex tells you the byte values of a colour. HSL tells you how a person would describe it: a hue (where on the colour wheel), a saturation (how vivid), and a lightness (how close to white or black). For a designer that maps to intuition, for a developer that maps to maths.
The day you write a button that needs a hover state five percent darker, HSL pays for itself. Knock five points off the lightness and you have the hover. Knock ten and you have the active. Drop saturation by twenty and you have the disabled. Doing the same with hex means three round trips through a colour picker and a designer who eyeballs each one.
Theming systems lean on HSL for the same reason. CSS custom properties hold a hue and saturation, the lightness varies for the shade scale. Tailwind v4 prefers OKLCH for perceptual uniformity, but HSL is still everywhere and is the smallest jump from hex.
The algorithm in plain words
Take the hex, convert each channel to a 0 to 1 float (so ff becomes 1.0, 88 becomes 0.533). Find the largest channel and the smallest. The lightness is the average of the two: L = (max + min) / 2.
If max equals min the colour is grey, hue and saturation are zero, you are done. Otherwise the saturation depends on the lightness. Below 0.5 the saturation is (max - min) / (max + min), at or above 0.5 it is (max - min) / (2 - max - min). The split is awkward but it falls out of the geometry, so live with it.
The hue is decided by which channel is the max. If red is highest, the hue is ((g - b) / (max - min)) * 60, with a wrap to keep it positive. If green is highest, ((b - r) / (max - min) + 2) * 60. If blue is highest, ((r - g) / (max - min) + 4) * 60. Three cases, each one rotates the colour wheel by 120 degrees.
The result is a hue from 0 to 360, saturation and lightness from 0 to 1. Multiply by 100 to express them as the percentages CSS expects.
CSS hsl() and alpha
Modern CSS accepts both hsl(35, 100%, 50%) (legacy comma form) and hsl(35 100% 50%) (modern space form). The space form composes with alpha as hsl(35 100% 50% / 0.5), which is the syntax you want for new code.
hsla() is still around for compatibility but the modern hsl() accepts the slash alpha and is preferred. If your tooling complains, that means it is older than 2022. Update.
Hue accepts a unit suffix. 35deg and 35 are the same. You can also pass turn (0.097turn), grad (38.9grad), or rad (0.611rad) when the maths is easier in those units. Most code paths stay with degrees because every other tool speaks degrees.
Designer and developer use
In practice the conversion is one direction (hex in, HSL out) and the consumer is a stylesheet that holds the hue and saturation as design tokens. Lightness is the variable. --brand: 217 91% 60%; lives in the root, and the button uses hsl(var(--brand)) for the base, hsl(var(--brand) / 0.85) for the hover, hsl(217 91% 50%) for the active.
The snippet below converts a hex to the HSL triplet. Save it in a build step, dump the design tokens once, and the rest of the stylesheet writes itself.
Working example
javascript// Hex to HSL. Returns { h: 0..360, s: 0..100, l: 0..100, a: 0..1 }.
function hexToHsl(hex) {
const m = hex.trim().replace(/^#/, "").toLowerCase();
const full =
m.length === 3 || m.length === 4 ? m.split("").map((c) => c + c).join("") : m;
if (!/^[0-9a-f]{6}([0-9a-f]{2})?$/.test(full)) return null;
const r = parseInt(full.slice(0, 2), 16) / 255;
const g = parseInt(full.slice(2, 4), 16) / 255;
const b = parseInt(full.slice(4, 6), 16) / 255;
const a = full.length === 8 ? parseInt(full.slice(6, 8), 16) / 255 : 1;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
let h = 0;
let s = 0;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
else if (max === g) h = ((b - r) / d + 2) * 60;
else h = ((r - g) / d + 4) * 60;
}
return {
h: Math.round(h),
s: Math.round(s * 100),
l: Math.round(l * 100),
a: Math.round(a * 1000) / 1000,
};
}
// hexToHsl("#3a86ff") -> { h: 217, s: 100, l: 61, a: 1 }
// As CSS: -> hsl(217 100% 61%) Just need the result?
Paste the hex into the colour converter on aldeacode.com and the HSL, OKLCH and HSV triplets land at once with the right CSS syntax. Copy the design token directly, no float maths in the head, no off by one on the hue rotation.
Open Color Converter (Hex / RGB / HSL) →Frequently asked questions
Why does my converted HSL not match my designer's value exactly?
Different tools round at different stages. Some round the floats before saturation, some after. Differences of one or two on hue or saturation are normal and visually identical. Use one source of truth (your build, the design file, or the converter) and stop comparing.
Should I use HSL or OKLCH for theming?
OKLCH is perceptually uniform, so a 10 point lightness drop looks the same darker across all hues. HSL is not, so a yellow at 50 percent looks lighter than a blue at 50 percent. For new design systems OKLCH wins. For existing CSS that already speaks HSL, the migration is rarely worth it on its own.
Can I store the HSL triplet without the hsl() wrapper?
Yes. CSS custom properties accept raw values, so --brand: 217 91% 60% works as long as you wrap it at use site: hsl(var(--brand)). This pattern lets you swap in alpha at the use site instead of the definition.