Command Palette

Search for a command to run...

Color Picker: HEX, RGB, HSL โ€” And the Contrast Check I Skipped

Color Picker: HEX, RGB, HSL โ€” And the Contrast Check I Skipped

T
Toolz Team
|Jul 3, 2026|17 min read

Our brand orange is #E99537. I like it. It's warm, it's not the same orange as everyone else's, and it sits next to our blue without a fight.

I used it as a link colour on white for months.

Then I actually ran the numbers. #E99537 on #FFFFFF gives a contrast ratio of 2.38:1. WCAG 2.1 SC 1.4.3 requires 4.5:1 for normal text. It doesn't even clear the 3:1 bar for large text. Every orange link on that site was, by the standard I claimed to care about, unreadable โ€” and it had looked perfectly fine to me, on my monitor, in my office, with my eyes.

That's the thing about colour: your perception of it is not a measurement. #E99537 on our navy #0F172A scores 7.50:1, comfortably AAA. Same colour, completely different verdict, and no amount of squinting at it would have told me. You have to compute it.

The Color Picker on toolz.dev converts between HEX, RGB, and HSL and shows you the contrast ratio, in the browser, with no upload. This guide is what I wish I'd read before I shipped those links.

TL;DR: Use HEX for static values, HSL whenever you need to derive a colour (hover states, palettes, dark mode) because lightness is a number you can subtract from, and RGBA/HSLA when you need alpha. Contrast is 4.5:1 for body text and 3:1 for large text โ€” check it, don't eyeball it. Color Picker does the conversions and the ratio. Then take your stops to the Gradient Generator.


HEX, RGB, HSL: What's the Actual Difference?

They all describe the same sRGB colours. What differs is which questions each one makes easy to answer.

HEX RGB HSL OKLCH
Looks like #4466EE rgb(68 102 238) hsl(228 83% 60%) oklch(58% 0.21 265)
Alpha form #4466EEcc rgb(68 102 238 / 80%) hsl(228 83% 60% / 80%) oklch(58% 0.21 265 / 80%)
Answers "what colour?" No โ€” you must decode it Barely Yes, hue is a number Yes
Answers "make it 10% darker" No No Yes, subtract from L Yes, and perceptually correctly
Perceptually uniform No No No Yes
Use it for Static values, design handoff Canvas, WebGL, JS maths Hover states, palettes, theming Modern palettes, gradient interpolation
Gotcha Unreadable to humans Verbose 50% lightness โ‰  equal brightness across hues Newer syntax; needs a fallback for old browsers

HEX

Three bytes in hexadecimal. #4466EE is red 44 (68), green 66 (102), blue EE (238). Shorthand #RGB works only when each pair is doubled โ€” #FF0000 collapses to #F00, but #4466EE cannot be shortened. The 8-digit form adds alpha: #4466EE80 is roughly 50% opaque.

It's the default in CSS because it's compact and unambiguous. It is also completely opaque to human reading, which is why I have never once looked at a hex code and known what to change to make it a little less saturated.

RGB

Same three channels, decimal, 0โ€“255. Modern CSS Color 4 syntax drops the commas: rgb(68 102 238 / 80%). RGB is what you want in JavaScript, canvas, and WebGL, because it's the format everything downstream actually consumes and you can do arithmetic on it.

HSL โ€” the one that earns its place

Hue (0โ€“360ยฐ around the wheel), saturation (0โ€“100%), lightness (0โ€“100%). #4466EE is hsl(228, 83%, 60%).

Here's why that matters. A button's hover state should be "the same colour, slightly darker." In HEX that's an unanswerable question. In HSL it's subtraction:

.btn {
  background: hsl(228 83% 60%);   /* #4466EE */
}
.btn:hover  { background: hsl(228 83% 52%); }  /* darker  */
.btn:active { background: hsl(228 83% 44%); }  /* darker still */
.btn:disabled { background: hsl(228 22% 70%); } /* desaturated, lifted */

Four states, one hue, no design tool. This is 90% of why HSL is in my toolkit.

The catch โ€” and it's a real one โ€” is that HSL lightness is not perceptual. hsl(60 100% 50%) (yellow) and hsl(240 100% 50%) (blue) both claim 50% lightness, and one of them will burn your retinas while the other is nearly black. That's the problem OKLCH solves.

OKLCH, briefly

CSS Color Level 4 brings oklch(L C H), where L is perceived lightness. Two colours with the same L genuinely look equally bright. It's the right tool for generating a tonal ramp where every step feels evenly spaced, and it's what I'd use for a new design system today. It's also why linear-gradient(in oklch, blue, yellow) doesn't go grey in the middle โ€” see the CSS Gradient Generator guide.

CMYK

Print only. CSS has no CMYK. If a brand guide hands you CMYK values, someone has to convert them to sRGB, and the conversion is approximate because the gamuts genuinely differ โ€” there are cyans a printer can hit that your monitor can't, and vice versa. Get both values defined in the brand guide rather than converting one to the other and hoping.


How Do I Convert Between Them?

HEX โ†’ RGB is just base conversion. #4466EE โ†’ split into 44, 66, EE โ†’ 68, 102, 238.

RGB โ†’ HSL involves finding max/min channels, deriving lightness from their average, then saturation and hue from there. It's fifteen lines of code, and I have written those fifteen lines from memory exactly zero times. For a one-off, the Color Picker does it instantly. For code:

const hexToRgb = (hex) => {
  const n = parseInt(hex.replace('#', ''), 16);
  return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
};

const rgbToHex = (r, g, b) =>
  '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('');

And the one that actually matters โ€” relative luminance and contrast ratio, straight from the WCAG definition:

const luminance = (hex) => {
  const { r, g, b } = hexToRgb(hex);
  const [R, G, B] = [r, g, b]
    .map(v => v / 255)
    .map(v => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
};

const contrast = (a, b) => {
  const [x, y] = [luminance(a), luminance(b)].sort((m, n) => n - m);
  return (x + 0.05) / (y + 0.05);
};

contrast('#E99537', '#FFFFFF'); // 2.38  โ† the one that got me
contrast('#E99537', '#0F172A'); // 7.50

Note those green coefficients: 0.2126 red, 0.7152 green, 0.0722 blue. Green carries roughly seventy percent of perceived brightness. That's why a bright green and a bright blue at identical HSL lightness look nothing alike, and it's the whole reason perceptual colour spaces exist.


What Contrast Ratios Do I Actually Have to Hit?

Level Normal text Large text (18pt / 24px, or 14pt / 18.66px bold) UI components & graphics
AA 4.5:1 3:1 3:1 (SC 1.4.11)
AAA 7:1 4.5:1 โ€”

Some real numbers, all computed rather than guessed:

Foreground Background Ratio Verdict
#000000 #FFFFFF 21.00:1 The ceiling
#333333 #FFFFFF 12.63:1 AAA
#595959 #FFFFFF 7.00:1 AAA, exactly
#767676 #FFFFFF 4.54:1 AA โ€” with 0.04 to spare
#999999 #FFFFFF 2.85:1 Fails everything
#4466EE #FFFFFF 4.79:1 AA for body text
#4466EE #0F172A 3.72:1 Large text only
#E99537 #FFFFFF 2.38:1 Fails โ€” my mistake
#E99537 #0F172A 7.50:1 AAA

Two things fall out of that table.

#767676 is the lightest grey that passes AA on white. Not #888, not #999. Every "muted secondary text" colour I see in the wild is a shade or two past it. Worth memorising.

The same brand colour can pass and fail depending on where you put it. Our orange is unusable as a link on white and excellent as an accent on navy. That isn't a flaw in the colour; it's a constraint on where the colour is allowed to live. Once I framed it that way, the design decision got easier, not harder.


How Do I Build a Palette That Doesn't Fight Itself?

Pick one hue. Vary lightness. That's the whole method, and HSL makes it mechanical:

:root {
  /* One hue: 228. Nine steps of lightness. */
  --blue-50:  hsl(228 83% 96%);
  --blue-100: hsl(228 83% 91%);
  --blue-200: hsl(228 83% 84%);
  --blue-300: hsl(228 83% 76%);
  --blue-400: hsl(228 83% 68%);
  --blue-500: hsl(228 83% 60%);  /* #4466EE โ€” the brand colour */
  --blue-600: hsl(228 83% 50%);
  --blue-700: hsl(228 78% 41%);
  --blue-800: hsl(228 72% 33%);
  --blue-900: hsl(228 66% 25%);
}

Notice saturation drops slightly at the dark end. Holding saturation constant while lightness falls produces steps that look increasingly harsh and neon โ€” a small desaturation at the bottom is what makes a ramp feel designed rather than generated.

Then name things by role, not by appearance, so a rebrand is a one-line change instead of a find-and-replace across forty files:

:root {
  --color-primary: var(--blue-500);
  --color-primary-hover: var(--blue-600);
  --color-text: #0F172A;
  --color-text-muted: #6B7280;   /* 4.83:1 on white โ€” checked */
  --color-surface: #F8FAFC;
  --color-border: #E5E7EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-primary: var(--blue-400);  /* lift it โ€” dark bg needs a lighter primary */
    --color-text: #F8FAFC;
    --color-text-muted: #9CA3AF;
    --color-surface: #1E293B;
    --color-border: #374151;
  }
}

Dark mode is not an inversion

Do not flip your palette. Two things break immediately:

  1. Saturated colours get louder on dark backgrounds. A vivid blue that reads as "professional" on white reads as "vibrating" on near-black. Lift lightness, drop saturation a little.
  2. Pure black backgrounds cause halation โ€” bright text on #000 smears for people with astigmatism. Use a very dark grey-blue (#0F172A, #111827) instead. It also gives you somewhere to go for elevated surfaces, which pure black doesn't.

Harmony schemes, honestly

Scheme What it means When I'd use it
Monochromatic One hue, many lightnesses Almost always. It's the ramp above.
Analogous Adjacent hues (blue + indigo + teal) Backgrounds and gradients that need to feel calm
Complementary Opposite hues (blue + orange) Exactly one accent. Ours is #4466EE / #E99537.
Triadic Three evenly spaced Data viz, where categories need to be distinguishable
Split-complementary One hue + two beside its opposite When straight complementary feels too aggressive

The rule I keep coming back to: one primary, one accent, a grey ramp. Everything beyond that has to justify its existence.


What Else Trips People Up?

Colour alone is never enough

Around 1 in 12 men and 1 in 200 women have some form of colour vision deficiency. If red-means-error and green-means-success is your only signal, a meaningful slice of your users see two identical grey badges. Add an icon, add a word. WCAG SC 1.4.1 is explicit about this and it costs you one <span>.

The one-second test: screenshot your UI, desaturate it, and see whether it still works. If it doesn't, colour is doing load-bearing work it shouldn't be doing alone.

Focus rings need contrast too

Focus indicators are UI components and fall under the 3:1 rule against adjacent colours โ€” which means a focus ring must contrast with both the element it surrounds and the page behind it. A subtle blue outline on a blue button is a common and completely invisible failure.

Design in grey first

Build the whole layout in greyscale. It forces hierarchy to come from size, weight, and spacing โ€” which is where hierarchy should come from. Add colour last, as emphasis. Interfaces built this way are accessible almost by accident, and they survive dark mode without a rewrite.


Using the Color Picker

  1. Open the Color Picker.
  2. Drag on the spectrum, or paste a known value into the HEX, RGB, or HSL field โ€” the others update in step.
  3. Note the HSL. That's your working format.
  4. Generate the ramp by holding H and S and stepping L.
  5. Check every text/background pair against the 4.5:1 line before it goes into a stylesheet, not after a client emails you about it.

Everything runs in your tab. Nothing is uploaded โ€” you can confirm that in the DevTools Network panel in about ten seconds.


Frequently Asked Questions

What's the difference between HEX and RGB?

Nothing, semantically โ€” they encode the same three sRGB channels. HEX writes them in base 16 (#4466EE), RGB in decimal (rgb(68 102 238)). HEX is more compact and is the CSS convention; RGB is easier to compute with in JavaScript. Pick per context, not per principle.

Which colour format should I use in CSS?

HEX for static values you're just transcribing. HSL any time you need to derive a colour โ€” hover states, disabled states, a tonal ramp, a dark-mode variant โ€” because lightness is a number you can adjust. HSLA or RGBA when you need alpha. OKLCH if you're building a new design system and want perceptually even steps.

How do I know if my colours are accessible?

Compute the contrast ratio. WCAG 2.1 AA needs 4.5:1 for normal text, 3:1 for large text (18pt, or 14pt bold) and for UI components. Do not judge by eye โ€” #E99537 on white looks fine and scores 2.38:1. Also check that no information is conveyed by colour alone.

What is the lightest grey I can use for text on white?

#767676, which lands at 4.54:1 โ€” barely over the 4.5:1 AA line. #999999 is only 2.85:1 and fails. If you want AAA, #595959 hits exactly 7.00:1.

Why does HSL lightness not match perceived brightness?

Because HSL is a simple mathematical transform of RGB, not a model of human vision. The eye is far more sensitive to green than to blue โ€” the WCAG luminance formula weights green at 0.7152 and blue at 0.0722 โ€” so hsl(60 100% 50%) (yellow) and hsl(240 100% 50%) (blue) look nothing alike despite both claiming 50% lightness. OKLCH exists specifically to fix this.

How do I build a dark mode palette?

Don't invert the light one. Build a second palette: a dark grey-blue background rather than pure black (pure black causes halation for readers with astigmatism), lighter and slightly less saturated versions of your accents, and text that's off-white rather than #FFFFFF. Swap them with CSS custom properties under prefers-color-scheme: dark.

What's the best colour for a call-to-action button?

There isn't one. The evidence for "orange converts best" is thin and mostly folklore. What matters is that the CTA contrasts with everything around it and that the same colour means the same thing everywhere on the site. If your page is mostly blue, an orange CTA works โ€” because of the contrast, not the orange.

Can I use CMYK colours on the web?

No โ€” CSS has no CMYK notation, and the gamuts don't map cleanly, so any conversion is an approximation. If you need brand consistency across print and screen, define both values in the brand guide from the start rather than converting one into the other.

How do I convert a HEX color to RGB?

Split the six-digit hex into three pairs and read each pair as a base-16 number from 0 to 255. #4466EE becomes rgb(68, 102, 238): 44 is 68, 66 is 102, and EE is 238. A three-digit shorthand such as #46E expands first by doubling each digit. The Color Picker does this conversion instantly, in both directions.

What is a hex color code?

A hex color code is a six-digit, base-16 representation of an sRGB colour, using two digits each for the red, green, and blue channels after a leading #. #FFFFFF is white and #000000 is black. An optional seventh and eighth digit add an alpha (opacity) channel, as in #4466EE80.


Wrapping Up

Colour work has two halves and most of us only do the first. Choosing a palette is the fun, subjective half. Verifying it โ€” luminance, contrast, colour-blind legibility, dark-mode behaviour โ€” is the half that's just arithmetic, and it's the half that decides whether people can actually read what you built.

The arithmetic is not hard. It's just something you have to do, and I didn't, for months, on my own site, in a colour I picked myself.

The Color Picker converts between HEX, RGB, and HSL and gives you the contrast ratio, free and entirely in your browser. Pair it with the Gradient Generator for blends that hold their contrast end to end, and see the rest of the shelf in the Developer Productivity Tools guide.

Comments

0 comments

0/2000 characters

No comments yet. Be the first to share your thoughts!