codr.tools
Tools

Color contrast checker

Check a foreground and background pair against the WCAG 2 contrast thresholds, composite transparency the way a browser does, find the nearest passing colour, and build a tint, shade and harmony palette with the contrast of every swatch.

Contrast checker

rgb(31, 111, 235) · hsl(216, 84%, 52%)

rgb(255, 255, 255) · hsl(0, 0%, 100%) · white

Anything below 100% is composited over the background before the ratio is measured, the way a browser paints it.

4.63:1

WCAG 2 contrast ratio, 4 of 5 checks passed

Contrast ratio 4.63:1. AA normal text: pass. AA large text: pass. AAA normal text: fail. AAA large text: pass. UI components and graphics: pass.

  • AA normal text · 4.5:11.4.3 Contrast (Minimum), level AAPass
  • AA large text · 3:11.4.3 Contrast (Minimum), level AAPass
  • AAA normal text · 7:11.4.6 Contrast (Enhanced), level AAAFail
  • AAA large text · 4.5:11.4.6 Contrast (Enhanced), level AAAPass
  • UI components and graphics · 3:11.4.11 Non-text Contrast, level AAPass
Preview

Normal text at 16px. Below 18pt, so AA asks for 4.5:1.

Large text: 24px bold

Large text: 18.66px bold (14pt)

A UI controlAn icon and a boundary: 1.4.11 asks for 3:1
Adjust
Which colour the suggestion moves

This pair already reaches 4.5:1. Pick a higher target to see a suggestion.

Palette from a base colour

rgb(31, 111, 235) · hsl(216, 84%, 52%)

Tints and shades

Same hue and saturation at eleven lightness levels. W and B are the contrast ratios against white and against black, so you can see at a glance which steps can carry text.

Complementary

The opposite hue, 180° away. The loudest pairing, good for one accent.

Analogous

The two neighbours 30° either side. Quiet, and easy to keep on brand.

Triadic

Three hues evenly spaced around the wheel, 120° apart.

Split complementary

The two hues either side of the complement. Contrast without the clash.

Check a foreground and background pair against the WCAG 2 contrast thresholds, composite transparency the way a browser does, find the nearest passing colour, and build a tint, shade and harmony palette with the contrast of every swatch.

How to use this tool#

  1. Set the two colours. Use the native picker for a quick choice, or type into the field beside it. Hex with 3, 4, 6 or 8 digits (the # is optional),rgb(), rgba(), hsl(), hsla() and CSS colour names such as rebeccapurple all work, in the comma syntax or the newer space syntax with a slash before the alpha.
  2. Read the ratio. The big number is the WCAG 2 contrast ratio, between 1:1 and 21:1, and it is rounded down, so a pair that reads 4.49 really is short of 4.5. The five plates beside it are the thresholds, each with its success criterion, and each marked with a word and an icon rather than colour alone.
  3. Drop the foreground opacity if your text or icon is semi-transparent. The colour is composited over the background first, because that is what the eye sees. The readout then shows the pair as it is actually painted.
  4. Swap to see the same pair the other way round. The ratio does not change, but the preview does, and light text on a dark panel reads differently from dark text on a light one.
  5. Ask for a passing colour. Pick a target, choose whether the foreground or the background should move, and the tool walks the lightness of that colour, keeping its hue and saturation, until the target is met. "Use it" puts the result back in the field.
  6. Build a palette. Give the second panel a base colour and it produces a 50 to 950 tint and shade scale plus four harmonies. Every swatch carries its contrast against white (W) and against black (B). Click a swatch to copy its hex, or send it to the checker as the foreground or the background.
  7. Export. The whole palette comes out as CSS custom properties, with your own variable prefix, ready to paste into a stylesheet.

What contrast ratio means#

Contrast ratio is a single number describing how far apart two colours are in lightness. WCAG 2 defines it as a ratio between 1:1, two identical colours, and 21:1, pure black against pure white. It is not a measure of how different two colours look in hue: a red and a green of the same lightness sit at roughly 1:1, however violently they clash.

The number matters because it is the yardstick in the Web Content Accessibility Guidelines, which is what procurement rules and accessibility statements point at. The United States Section 508 standards incorporate WCAG 2.0 Level AA by reference, and EN 301 549, the European standard behind public sector and European Accessibility Act obligations, currently references WCAG 2.1 Level AA. The contrast requirement is word for word the same in both. It is also a decent proxy for whether text survives a cheap laptop screen, glare on a phone, or a reader whose eyesight is not what it was.

How the ratio is computed#

Two steps. First the relative luminance of each colour, which WCAG 2.2 defines for sRGB asL = 0.2126 * R + 0.7152 * G + 0.0722 * B, where each channel is first divided by 255 and then linearised: if the value is at most 0.04045 it is divided by 12.92, otherwise it becomes ((value + 0.055) / 1.055) ^ 2.4. Green carries most of the weight because the eye is most sensitive to it, blue the least.

Then the ratio itself, (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter of the two and L2 the darker. The 0.05 is not arbitrary: WCAG's own notes on the formula say that "the ANSI/HFS 100-1988 standard calls for the contribution from ambient light to be included in the calculation of L1 and L2", and that "the .05 value used is based on Typical Viewing Flare" from the sRGB specification. It is also what keeps the scale finite, since without it anything against pure black would be infinite.

javascript
// WCAG 2 relative luminance, for one 8-bit sRGB colour.
function channel(value) {
  const c = value / 255;
  return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
}

function luminance({ r, g, b }) {
  return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
}

// The ratio: lighter first, darker second, both offset by 0.05.
function contrast(a, b) {
  const [light, dark] = [luminance(a), luminance(b)].sort((x, y) => y - x);
  return (light + 0.05) / (dark + 0.05);
}

contrast({ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 }); // 21

0.04045 or 0.03928?

Both numbers are in circulation, and this tool uses 0.04045, the value in the current text. WCAG's own note explains the change: "Before May 2021 the value of 0.04045 in the definition was different (0.03928). It was taken from an older version of the specification and has been updated. It has no practical effect on the calculations in the context of these guidelines."

For 8-bit colour it has no effect whatsoever, not just no practical effect. The two thresholds sit at 10.3 and 10.0 on the 0 to 255 scale, and every channel value from 0 to 10 falls below both of them, while 11 and up clear both. The unit test beside this tool checks all 256 values and finds no difference, which is why the figures here match older calculators digit for digit.

The success criteria involved#

Three WCAG 2 success criteria set the thresholds this tool reports. All three are quoted fromWCAG 2.2, the current W3C Recommendation; the wording is unchanged from WCAG 2.1 and, for the first two, WCAG 2.0.

  • 1.4.3 Contrast (Minimum), level AA: text and images of text have a ratio of at least 4.5:1, and large-scale text at least 3:1. This is the line nearly every accessibility policy draws.
  • 1.4.6 Contrast (Enhanced), level AAA: the same rule at 7:1 for normal text and 4.5:1 for large text.
  • 1.4.11 Non-text Contrast, level AA: at least 3:1 against adjacent colours for "visual information required to identify user interface components and states" and for "parts of graphics required to understand the content". This is the one people forget: the border of an input, a focus ring, the bar of a chart, the line of an icon that carries meaning.

Level A has no contrast requirement at all. AA is the usual target, and AAA is worth reaching for on body text when the design allows it, but not as a blanket rule: WCAG's conformance section says plainly that "it is not recommended that Level AAA conformance be required as a general policy for entire sites because it is not possible to satisfy all Level AAA success criteria for some content".

What counts as large text#

WCAG defines large scale text as "at least 18 point or 14 point bold or font size that would yield equivalent size for Chinese, Japanese and Korean (CJK) fonts". Points, not pixels, because the definition predates the web's settling on CSS pixels.

The translation to CSS is arithmetic rather than opinion: CSS defines 1in as exactly 96px and 1pt as 1/72in, so 18pt is 24px and 14pt bold is 18.66px bold. Those are the two sizes in the preview above. WCAG adds its own rule of thumb in a note: "For many mainstream body text fonts, 14 and 18 point is roughly equivalent to 1.2 and 1.5 em or to 120% or 150% of the default size for body text (assuming that the body font is 100%), but authors would need to check this for the particular fonts in use."

That caveat is real. Point size is a property of the font's em box, not of the letters, so a condensed or small-x-height face at 24px can be visibly smaller than a generous one at 20px. If a size is borderline, treat it as normal text and aim for 4.5:1.

What is exempt#

1.4.3 and 1.4.6 both carve out the same three cases, in these words:

  • Incidental: "text or images of text that are part of an inactive user interface component, that are pure decoration, that are not visible to anyone, or that are part of a picture that contains significant other visual content, have no contrast requirement". A greyed-out disabled button is the everyday example.
  • Logotypes: "text that is part of a logo or brand name has no contrast requirement". The logo is exempt. The tagline next to it is not.
  • Large text drops to 3:1 under 1.4.3 and to 4.5:1 under 1.4.6, as above.

1.4.11 has its own exceptions: components whose appearance "is determined by the user agent and not modified by the author", inactive components, and graphics where "a particular presentation is essential to the information being conveyed", such as a screenshot or a flag.

Transparency, gradients and photographs#

WCAG 2 says nothing about how to compute a colour that is semi-transparent. The criteria talk about the contrast of what is presented, and the techniques behind them (G18, G145 and the failure F83) simply require the ratio to hold against the background as it is rendered. There is no compositing formula in the specification.

So the tool does what the browser does: source-over alpha compositing. A colour at opacitya over a backdrop becomes top * a + bottom * (1 - a) per channel, and the ratio is measured on the result. Black at 50% on white is not black on white, it is#808080 on white, which is 3.94:1 and fails AA for body text.

javascript
// Source-over: what the screen actually shows when text is
// painted at 50% opacity on a white background.
function composite(fg, bg) {
  const mix = (top, bottom) => top * fg.a + bottom * (1 - fg.a);
  return { r: mix(fg.r, bg.r), g: mix(fg.g, bg.g), b: mix(fg.b, bg.b) };
}

composite({ r: 0, g: 0, b: 0, a: 0.5 }, { r: 255, g: 255, b: 255 });
// { r: 128, g: 128, b: 128 } -> 3.94:1 against white, not 21:1

If the background itself is translucent, this tool flattens it over white before compositing the foreground on top, and says so under the ratio. On a real page the backdrop would be whatever sits behind, so check the pair you actually ship.

Text over a gradient or a photograph is harder, because there is no single background colour. The practical method is to sample the worst pixel under the text and check that, or to remove the problem by putting a solid or near-solid plate behind the text. If you are building the background here, the CSS gradient generator gives you the stop colours to test: check the text against every stop it crosses, not just the middle. The WCAG 3 draft acknowledges the same gap in a note of its own: "Transparency can cause testing issues, but should be tested as the rendered color."

Where the WCAG 2 formula falls short#

The formula is simple, stable and testable by a machine, which is exactly why it became the standard. It is also a model of a screen from the 1990s, and it has known rough edges:

  • It is polarity-blind. Dark text on light and light text on dark score the same, although most people find them differently readable, and the difference grows with thinner type.
  • It knows nothing about weight or size beyond one step. There is one threshold for normal text and one for large, with a hard edge between them. A 300-weight 17px face and a 700-weight 17px face are judged identically.
  • Dark pairs bunch up. The 0.05 offset compresses differences at the dark end, so two very dark colours can score higher than they look, which shows up most on dark themes.
  • It assumes sRGB and a reference viewing condition. Real displays run far brighter than the 80 cd/m² that sRGB assumes, and a wide-gamut colour has to be converted to sRGB before the formula means anything.

These criticisms come from the accessibility community and from the WCAG 3 working group's own discussions, not from a formal W3C statement conceding error. WCAG 2 remains the standard that conformance is measured against, so a ratio that clears 4.5:1 clears it, whatever a newer model would say.

Where APCA stands, accurately

APCA, the Advanced Perceptual Contrast Algorithm, is the best known attempt at a replacement. It models polarity, font size and weight, and returns a lightness contrast value rather than a ratio. It is worth knowing about. It is not a standard, and the difference matters if you are writing a conformance claim:

  • APCA is not part of any published W3C Recommendation. WCAG 2.2 is the current Recommendation, and it uses the ratio described above.
  • WCAG 3.0 is a Working Draft, which states of itself: "It is inappropriate to cite this document as other than a work in progress." It has no date and no guarantee of becoming a Recommendation.
  • In the 10 September 2026 Working Draft the contrast requirement reads "The default visual presentation of text meets @@[contrast measure to be determined]", with an editor's note saying "The contrast algorithm used in WCAG 3 is yet to be determined". APCA is not named anywhere in that draft's text.
  • Its reference implementation is licensed to W3C under a cooperative agreement for WCAG use and to everyone else under AGPL v3, with a patent-pending notice. That licensing has been raised as a concern within the working group and is a separate obstacle to adoption from the technical question.

Practical reading: build to WCAG 2 thresholds, which is what audits and contracts ask for, and treat APCA as a second opinion when a pair passes 4.5:1 but still looks thin, typically light grey text on a dark panel.

Designing a palette with contrast in mind#

The usual mistake is to choose a brand colour, then discover later that nothing readable can sit on it. Working from a scale avoids that, which is what the second panel is for.

  • Pick text colours by ratio, not by eye. On a light theme, a 700 or 800 step usually clears 4.5:1 on white while the 500 step does not. The W and B numbers on each swatch tell you before you commit.
  • Find the flip point. Somewhere in the middle of the scale, white text becomes better than black text. That step is where a badge or a filled button has to change its label colour.
  • Hold borders to 3:1. Under 1.4.11 the boundary of an input or a button needs 3:1 against what is next to it, which usually means a 400 to 600 step rather than a 200.
  • Harmonies are a starting point, not an answer. Rotating the hue keeps the lightness roughly the same, so a complement of a mid-tone is also a mid-tone, and the two will have poor contrast with each other. Use the scale to separate them.
  • Build both themes at once. A colour that passes on white rarely passes on a near-black panel. Usually the dark theme needs a lighter step of the same hue, not the same value inverted.

The export gives you the whole thing as custom properties:

css
:root {
  /* Tints and shades */
  --brand-50: #f1f6fe;
  --brand-100: #deeafc;
  --brand-200: #bdd5f9;
  --brand-300: #93baf5;
  --brand-400: #6099f1;
  --brand-500: #2c78ec;
  --brand-600: #135ed3;
  --brand-700: #0f4ba9;
  --brand-800: #0b387e;
  --brand-900: #082859;
  --brand-950: #051938;

  /* Complementary */
  --brand-complementary-2: #eb9b1f;
}

Worked examples#

The two greys everyone runs into

#767676 on white is 4.54:1 and passes AA. One step darker in hex,#777777, is 4.47:1 and fails it. That is why #767676turns up in so many design systems: it is the lightest plain grey that carries body text on white. For AAA you need #595959, which lands at 7.00:1, and the suggestion tool above walks you from one to the other.

Pure red is not a text colour

#ff0000 on white is 3.99:1. It fails AA for body text by a hair, passes for large text, and passes 1.4.11 for a border or an icon. White text on that same red is also 3.99:1, since the ratio is symmetric, while black text on it is 5.25:1. A red error message at 16px needs to be darkened, not made bold.

A 50% overlay

Black text at 50% opacity on white looks grey because it is grey: the composite is#808080, and the ratio is 3.94:1, a fail for body text. The same goes for the fashionable trick of setting a caption to rgba(255, 255, 255, 0.7) on a coloured panel. On a mid blue such as #1f6feb that comes out at3.07:1, which is enough for large text and not for a caption.

Nothing here leaves your browser#

Every colour you type, every ratio and the whole palette are computed on your machine after the page has loaded. There is no backend to send them to: this site is static files. Disconnect from the network and the tool keeps working.

The maths is our own code, with no colour library, and it is covered by unit tests that check the reference values quoted on this page, including the exact 21:1 of black on white and the two greys above.

Questions people ask#

Where do 4.5:1 and 7:1 come from?

From ergonomics standards plus one correction for eyesight. WCAG'srationale for the ratios explains that 3:1 is "the minimum level recommended by [ISO-9241-3] and [ANSI-HFES-100-1988] for standard text and vision", and that a person with 20/40 acuity loses contrast sensitivity by a factor of roughly 1.5, so 3 times 1.5 gives 4.5:1. The same logic at 20/80 acuity gives the AAA figure of about 7:1. WCAG adds that "20/40 is commonly reported as typical visual acuity of elders at roughly age 80".

So 4.5:1 is a floor, not a target: it covers a specific, fairly moderate amount of vision loss and nothing beyond it. If your readers skew older, or the text is small, thin, or sits over anything busy, aim higher.

Does the ratio change if I swap the colours?

No. The formula sorts the two luminances, so light on dark and dark on light give the same number. What changes is how it reads: light text on a dark panel tends to look bolder and can smear at small sizes, which is one of the things the WCAG 2 formula does not capture.

Do I have to test disabled buttons and placeholder text?

Disabled controls are exempt under the Incidental exception, and under 1.4.11 as inactive components. Placeholder text is not exempt, and that is stated outright: the Understanding document for 1.4.3 says the criterion "applies to text in the page, including placeholder text and text that is shown when a pointer is hovering over an object or when an object has keyboard focus". Hold it to 4.5:1, and give the field a real label as well.

My colour is in OKLCH or P3. Can I check it?

Not directly here: this tool reads hex, rgb(), hsl() and colour names, and the WCAG formula is defined for sRGB. Convert the colour to its sRGB hex first, for example with the dev tools colour picker, and check that. A colour outside the sRGB gamut has no well-defined WCAG luminance until it is mapped into it.

Why does the ratio round down?

Because rounding up would turn a 4.4996 into a passing 4.5. Every value here is truncated at two decimals and the pass and fail marks are decided on the full-precision number, so what you see and what the badges say never disagree.

What does the suggestion actually change?

Only lightness. It keeps the hue, the saturation and the alpha of the colour you asked it to move, and walks the lightness in both directions in small steps until the target ratio is met, then returns whichever side needed the smaller move. When no lightness of that colour can reach the target, which happens with a very transparent foreground or a mid-grey background, it says so instead of returning something that does not pass.

Is a contrast checker enough to call a design accessible?

No. It checks one thing. Colour that carries meaning on its own (1.4.1), text that has to survive being resized to 200% (1.4.4), focus that has to be visible (2.4.7) and a dozen other criteria are all outside what any ratio can tell you. Contrast is simply the one that is easy to measure, so it is the one worth getting right first.

  • Build linear, radial and conic CSS gradients visually: any angle, shape, size and centre, colour stops with their own position and opacity, repeating variants and an interpolation colour space, with the background declaration ready to copy.

  • Build a CSS box-shadow visually, with as many layers as you need: offsets, blur, spread, colour with opacity and inset, over a preview whose box colour, background and border radius you set yourself.

  • Build a CSS Grid layout visually: set the columns, rows, track sizes and gaps, drag items across cells to place them, and copy the generated CSS and the matching HTML.

  • Convert identifiers and text to camelCase, PascalCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, Train-Case, dot.case, path/case and four more at once, one line at a time, with acronyms and digits split the way style guides expect.