nicetool.dev logo
#accessibility#css#design

WCAG Color Contrast Explained: Why 4.5:1 Matters and How to Pass It

What the WCAG contrast ratio measures, the exact AA and AAA thresholds, the common traps (rounding, transparency, dark mode, text on images) and quick fixes for colors that fail.

6 min read

Light gray text on a white background looks clean in a design mockup. On a phone in the sun, on a cheap office monitor or for anyone over forty, it can be close to unreadable. The WCAG contrast ratio turns "is this readable?" into a number you can test in code review. This article explains where the number comes from, which thresholds apply, and the mistakes that make a design fail even when the designer "checked it".

What the contrast ratio measures

WCAG 2.x compares the relative luminance of two colors, which is how bright a color appears to the human eye on a scale from 0 (black) to 1 (white). It is not the same as the "L" in HSL. Pure yellow and pure blue both have 50% lightness in HSL, yet yellow is about 13 times more luminous.

Luminance is calculated in three steps:

  1. Convert each sRGB channel from 0-255 to 0-1.
  2. "Linearize" it to undo the gamma curve: values up to 0.04045 are divided by 12.92, larger values use ((c + 0.055) / 1.055) ^ 2.4.
  3. Weight the channels by how sensitive the eye is to them: L = 0.2126 R + 0.7152 G + 0.0722 B.

Green carries most of the weight, blue very little. That is why dark blue text works well on white, while light green text almost never does.

The ratio between a lighter color L1 and a darker color L2 is:

ratio = (L1 + 0.05) / (L2 + 0.05)

The 0.05 represents ambient light reflecting off the screen. The result ranges from 1:1 (identical colors) to 21:1 (black on white).

The thresholds you need to know

Requirement Level Minimum ratio
Normal text (1.4.3) AA 4.5:1
Large text (1.4.3) AA 3:1
Normal text (1.4.6) AAA 7:1
Large text (1.4.6) AAA 4.5:1
UI components and graphics (1.4.11) AA 3:1

"Large text" means at least 24px regular weight, or at least 18.66px bold (18pt and 14pt in the original wording). A 20px semibold heading is not large text, so it needs 4.5:1.

AA is the level almost everyone targets. It is the level referenced by laws and standards such as the European standard EN 301 549 (used by the European Accessibility Act) and many government accessibility rules. AAA is a good goal for body text but is not usually required for a whole site.

Success criterion 1.4.11 (added in WCAG 2.1) is often forgotten. Input borders, checkbox outlines, focus rings, icons that carry meaning and chart lines all need 3:1 against the colors next to them. A form field with a #e5e7eb border on white (1.24:1) is practically invisible to many users.

Some things are exempt: disabled controls, purely decorative elements, text that is part of a photo, and logos.

Abstract rules are easier to remember with concrete values. These ratios are calculated with the formula above:

Foreground Background Ratio Normal text AA
#777777 gray white 4.47:1 Fails
#767676 gray white 4.54:1 Passes
#999999 gray white 2.85:1 Fails
white #007bff (Bootstrap 4 primary) 3.98:1 Fails
white #0d6efd (Bootstrap 5 primary) 4.50:1 Passes, just
white #3b82f6 (Tailwind v3 blue-500) 3.68:1 Fails
white #2563eb (Tailwind v3 blue-600) 5.17:1 Passes
white #22c55e (Tailwind v3 green-500) 2.28:1 Fails
white #15803d (Tailwind v3 green-700) 5.02:1 Passes
#9ca3af (Tailwind v3 gray-400) white 2.54:1 Fails

Two lessons stand out. First, #767676 is the lightest neutral gray that passes on pure white, so anything lighter fails for body text. Second, the classic "500" shade of most palettes is too light for white button labels. Bootstrap 5 uses a slightly darker primary blue than Bootstrap 4, and that small change is enough to cross the 4.5:1 line.

Green buttons are a special trap. Because green dominates luminance, a green that looks saturated and dark is usually still too light for white text. You often need to go two steps darker than your instinct says.

Five common mistakes

1. Rounding up

WCAG thresholds are strict. A ratio of 4.47:1 does not pass, even though some tools display it as "4.5". Our Color Contrast Checker truncates instead of rounding, so a passing result is a real pass.

2. Ignoring transparency

Designers love rgba(0, 0, 0, 0.5) for secondary text. Its contrast depends entirely on what is underneath. On white, 50% black becomes #808080, which gives only 3.95:1 and fails. Always blend a translucent color with its actual background before checking it. The same applies to opacity on a parent element.

3. Placeholder and hint text

Placeholder text is still text. Many CSS resets and UI kits use a light gray placeholder around 2.5:1. If a placeholder carries information, such as the expected format "DD/MM/YYYY", it needs 4.5:1. Better still, put the information in a visible label or hint below the field.

4. Forgetting dark mode

Each theme is a separate design and needs separate checks. Tailwind v3 slate-500 (#64748b) on white gives 4.76:1 and passes, but the same color on slate-900 (#0f172a) gives only 3.75:1. In dark mode you usually need to move one or two steps lighter, for example to slate-400 (6.96:1).

5. Text over images and gradients

A hero title over a photo might pass on the dark part of the image and fail on a bright patch of sky. Check the contrast against the lightest area behind the text (or the darkest, for dark text). The reliable fix is a semi-transparent overlay or a text shadow, plus a solid background color as a fallback while the image loads.

How to fix a failing color

When a pair fails, you rarely need to change the whole palette:

  • Adjust lightness, keep the hue. Darken the foreground (or lighten the background) in small steps until it passes. The hue stays recognizable as your brand color. The contrast checker on nicetool.dev suggests the closest passing color automatically.
  • Make the text large. If a color must stay as it is (for example a brand color on a banner), using it only for text of 24px and above lowers the requirement to 3:1.
  • Swap the roles. A light brand color that fails as text often works well as a background with dark text on top.
  • Use two tokens. Keep brand-500 for decorative fills and add a brand-700 token for text and icons. Most design systems end up with this split.

When you change a color, convert it to the format your codebase uses (HEX, RGB, HSL or OKLCH) with the Color Converter so that design tokens stay consistent.

What about APCA and WCAG 3?

You may have heard of APCA (Accessible Perceptual Contrast Algorithm). It models perception more accurately than the WCAG 2.x formula. For example, it accounts for font weight and treats light-on-dark differently from dark-on-light. APCA is being developed for the future WCAG 3, which is still a draft. Until it becomes a standard, WCAG 2.x ratios remain the requirement for audits and legal compliance. You can explore APCA in design work, but still test against 4.5:1 and 3:1.

A quick checklist

  • Body text and labels: 4.5:1 or more.
  • Text 24px+ (or 18.66px+ bold): 3:1 or more.
  • Borders of inputs, focus rings, meaningful icons: 3:1 against adjacent colors.
  • Check hover, focus, error and disabled states, not only the default state.
  • Check light and dark themes separately.
  • Blend transparent colors before measuring.
  • Never round 4.4x up to 4.5.

Contrast is one of the cheapest accessibility wins. It needs no extra code, only better color choices made early. Paste your foreground and background into the Color Contrast Checker to see the ratio, the AA and AAA results and a suggested fix in seconds. Everything runs in your browser.

Written by

Nguyễn Thành Nam

Full-Stack Engineer · Backend & Platform

Full-stack engineer with 9+ years of experience building high-traffic APIs and platforms with Go, NestJS, PHP (Laravel, Phalcon) and Node.js. Specializes in GraphQL API design, real-time systems and performance optimization, and builds NICETOOL.dev for the developer community.

Try it yourself

Related articles