Calci.inCalculate Today for a Better Tomorrow

Color Picker

Hex, RGB, HSL and contrast checking

The guide

Colour formats, and checking contrast properly

How hex, RGB, HSL and OKLCH relate, why lightness in HSL lies, and what the WCAG contrast ratios actually require.

Last reviewed · 1,356 words

In short

  • Hex and RGB are the same thing written differently. HSL is a rotation of the same space that is easier to reason about.
  • HSL lightness is not perceived lightness. Yellow and blue at 50% lightness look nothing alike.
  • WCAG requires 4.5:1 for body text and 3:1 for large text. Both are calculated from relative luminance, not from how different two colours look.
  • Contrast ratio ignores hue entirely, so two colours can pass the ratio and still be unreadable together.
  • OKLCH is perceptually uniform, which makes generating a consistent palette straightforward in a way HSL never was.

Every colour format on the web describes the same thing — how much light of each primary a screen should emit — and they differ in how easy that is to think about.

Hex and RGB

#FF5733 is three bytes: red FF (255), green 57 (87), blue 33 (51). Written as RGB that is rgb(255, 87, 51).

They are the same value. Hex is compact and RGB is readable, and neither tells you anything useful about the colour without doing the arithmetic in your head.

Shorthand hex expands each digit: #F53 is #FF5533. An eight-digit hex adds alpha: #FF573380 is the same colour at 50% opacity.

HSL, and where it misleads

HSL rearranges the same space into three axes people can reason about.

Hue, 0 to 360 degrees around a colour wheel: 0 red, 120 green, 240 blue. Saturation, 0% grey to 100% fully saturated. Lightness, 0% black to 100% white, with 50% the pure hue.

This is far better for adjusting a colour — darkening a button on hover is one number in HSL and three in RGB.

The problem is that HSL lightness is not perceptual. hsl(60, 100%, 50%) is pure yellow and hsl(240, 100%, 50%) is pure blue, and they are wildly different in apparent brightness. The yellow is nearly white; the blue is nearly black.

Their relative luminances are 0.928 and 0.072 — a contrast ratio of 8.0:1 between two colours HSL claims are equally light.

This is why a palette built by holding lightness constant and rotating the hue produces colours that look nothing like a set, and why text that passes contrast on one hue fails on another at the same HSL lightness.

Contrast, and how it is calculated

WCAG contrast ratio is computed from relative luminance, which weights the channels by how sensitive human vision is to each:

L = 0.2126 R + 0.7152 G + 0.0722 B

after each channel has been linearised from its sRGB value.

Green carries 71% of the weight and blue only 7%. That is not arbitrary — the eye has far more green-sensitive cones than blue.

The ratio is (L1 + 0.05) ÷ (L2 + 0.05), lighter over darker. It runs from 1:1 for identical colours to 21:1 for black on white.

What the levels require

ContentAAAAA
Body text4.5:17:1
Large text (18pt, or 14pt bold)3:14.5:1
UI components and graphics3:1
Logos and decorative textExemptExempt

AA is the practical standard and the one most legal accessibility requirements reference. AAA is worth aiming for in body text where the design allows.

Two consequences worth internalising.

The light grey hint text inside form fields almost always fails. #999999 on white is 2.85:1 — below even the large-text threshold. Field hints, disabled states and secondary labels are where contrast failures cluster, because designers reach for grey to signal de-emphasis.

Large text has a lower bar for a reason. Bigger glyphs have thicker strokes and more area, so they remain legible at lower contrast. The threshold is 18pt regular or 14pt bold — roughly 24px and 18.66px in CSS.

What contrast ratio does not capture

The formula reduces two colours to two numbers, and it discards hue entirely.

Two colours can pass and still be unreadable. Saturated red text on saturated blue at a passing ratio produces chromatic aberration — the eye focuses the two wavelengths at slightly different depths and the text appears to vibrate.

Colour blindness is invisible to the ratio. Around 8% of men have some form of red-green deficiency. Two colours with adequate contrast can be indistinguishable to them if the difference is carried by hue rather than lightness. The rule that follows: never use colour alone to convey information — pair it with a shape, a label or a pattern.

Thin fonts fail in practice. A 300-weight font at 4.5:1 is harder to read than a 400-weight at the same ratio, and the formula cannot see it.

APCA, the algorithm proposed for WCAG 3, accounts for font weight and size and models perception better. It is not yet the standard, and AA remains what to build against.

OKLCH, and why it is worth learning

oklch(70% 0.15 30) uses lightness, chroma and hue like HSL — and its lightness is perceptually uniform.

Two OKLCH colours at the same lightness genuinely look equally light, whatever their hue. That single property fixes what HSL gets wrong, and it makes several things straightforward:

A palette of consistent weight. Hold lightness, vary hue, and every colour sits at the same visual level.

Predictable dark mode. Inverting lightness produces a sensible result rather than a muddy one.

Reliable interpolation. Gradients between OKLCH colours do not pass through the grey dead zone that RGB interpolation produces.

Wider gamut. It can express colours outside sRGB for displays that support them, with defined fallback behaviour.

Browser support is now broad enough to use directly, and the practical approach is to define palette tokens in OKLCH and let the browser handle the rest.

Practical colour advice

Do not use pure black on pure white. 21:1 is harsher than it needs to be, and most well-designed interfaces use something near #1a1a1a on #fafafa — still comfortably above 16:1 and easier on the eye.

Test with a real contrast checker, not by eye. The failures are consistently in places that look fine to someone with good vision on a good screen.

Check both themes. A palette that passes in light mode frequently fails in dark, because the relationship between the tokens inverts.

Check every state. Hover, focus, disabled, error and visited each have their own colours and each needs to pass.

Give the focus indicator 3:1 against its background. Keyboard users depend on it, and a subtle focus ring is a genuine accessibility failure.

Building a palette

A workable palette has fewer colours than most people start with.

One brand colour and a scale of tints and shades derived from it. Nine steps from very light to very dark covers almost every use — background tints, borders, text, hover states.

A neutral scale, which is where most of the interface actually lives. Backgrounds, borders, body text and secondary text are all neutrals, and getting this scale right matters more than the brand colour.

Semantic colours for success, warning, error and information. Each needs a text-weight version and a background-weight version, because the same green cannot serve as both a label colour and a banner background at accessible contrast.

The mistake worth avoiding: defining a colour for one purpose and reusing it for another at a different contrast requirement. A green that reads well as a filled badge fails as text on white, and discovering that late means changing a token everywhere.

Build the scale in OKLCH with even lightness steps, then check the text-weight entries against their intended backgrounds before treating the palette as done.

Dark mode is not an inversion

Flipping every colour produces something harsh and usually wrong.

Do not use pure black. A very dark grey — around #121212 to #1a1a1a — reduces the halation effect that makes light text on pure black appear to smear, particularly on OLED screens.

Desaturate. Colours that work on white are frequently too intense on dark. The same hue at lower chroma reads better.

Reduce contrast slightly. Where light mode uses near-black on near-white at 16:1, dark mode is generally more comfortable somewhere between 12:1 and 15:1.

Elevation is expressed by lightness, not by shadow. A raised surface in dark mode is a slightly lighter grey, because a shadow on a dark background is invisible.

Recheck every contrast pair. A palette that passes in light mode routinely fails in dark, because the lightness relationships between tokens are not symmetric.

What this tool assumes

  • sRGB colour space, which is what nearly every screen and every CSS colour without a space specified uses.
  • Contrast ratios follow the WCAG 2 formula, using relative luminance with the standard channel weights.
  • The large-text threshold is 18pt or 14pt bold, translated to CSS pixels.
  • Alpha transparency is not composited — a semi-transparent colour's real contrast depends on what is behind it.
  • Everything runs in your browser. Nothing is sent anywhere.

Sources

Frequently asked questions

What is the difference between hex, RGB and HSL?

They describe the same colour differently. Hex and RGB both give red, green and blue channels, one in base 16 and one in decimal. HSL gives hue, saturation and lightness, which is far easier to adjust by hand: changing lightness in HSL is one number, while in hex it is three.

What contrast ratio do I need?

4.5:1 for body text and 3:1 for large text to meet WCAG AA, and 7:1 for AAA. The ratio is computed from relative luminance rather than from how different two colours look, which is why two colours that seem distinct can still fail.

Why does my colour fail on white but pass on black?

Because contrast depends on luminance, and a mid-tone colour sits far from neither. A medium blue can fail against white and pass against black at the same size, which is why both are shown.

What is a complementary colour?

The hue directly opposite on the colour wheel, 180 degrees away. It gives the strongest contrast of hue, which is useful for an accent and exhausting across a whole interface.