Your brand purple looked perfect in Figma. On the live site, body text on that hero background scores 2.8:1. WCAG AA requires 4.5:1 for normal text. Users with low vision already bounced before they read the headline.
Contrast is a number, not a vibe. Here is how to audit a brand palette without guessing.
What is WCAG color contrast?
WCAG (Web Content Accessibility Guidelines) defines contrast ratio as the difference in relative luminance between two colors. The formula uses sRGB values, not how "different" the hues look.
Two colors can feel distinct and still fail if their lightness is too close. A saturated orange on a red background might look fine to you and score 2.1:1.
Minimum ratios in WCAG 2.1 and 2.2:
| Text type | AA | AAA |
|---|---|---|
| Normal body text | 4.5:1 | 7:1 |
| Large text (at least 18pt, or 14pt bold) | 3:1 | 4.5:1 |
| UI components and graphical objects | 3:1 | — |
Those numbers are Success Criterion 1.4.3 Contrast (Minimum) at Level AA and 1.4.6 Contrast (Enhanced) at Level AAA. Non-text contrast is 1.4.11, Level AA only, which is why the AAA cell is empty. WCAG 2.2 did not change these three criteria. The same thresholds are in WCAG 2.1.
Most product teams target AA for body copy. AAA is worth chasing for long-form reading or regulated industries, but AA is the baseline most audits expect.
How to check contrast against WCAG 2.1 and 2.2
The ratio is a formula, so the check is a sequence, not a glance at two swatches.
- Name the pair you actually ship. Body text on the page background, the link color on that same background, the button label on the button fill, the icon on its chip. A brand purple measured against white does not answer any of those if the purple never sits on white.
- Use the rendered color. Opacity, a scrim, or a token that is not the hex in the brand PDF all change the channels. Copy the computed hex from the browser, or the token that CSS will resolve to. Do not guess the mix.
- Convert each sRGB channel to relative luminance. WCAG defines relative luminance from the sRGB values: each 8-bit channel is divided by 255, linearized (divide by 12.92 if the value is 0.04045 or below, otherwise apply the 2.4 gamma curve), then combined as
0.2126 R + 0.7152 G + 0.0722 B. - Compute the ratio.
(L1 + 0.05) / (L2 + 0.05), where L1 is the lighter luminance and L2 is the darker one. The ratio runs from 1:1 (identical colors) to 21:1 (black and white). - Pick the threshold for that text. Under 1.4.3, normal text needs 4.5:1 for AA. Large-scale text needs 3:1. Under 1.4.6, those bars rise to 7:1 and 4.5:1. Under 1.4.11, a control boundary or a meaningful icon needs 3:1 against the adjacent color. The contrast checker reports the ratio and the AA/AAA results so you are not doing the gamma step by hand.
- Apply the large-text rule only when the type qualifies. WCAG's definition of large-scale text is at least 18 point, or at least 14 point bold. W3C technique G18 notes that 1pt equals 1.333 CSS pixels, so those sizes are about 24px and about 18.5px. The understanding document for 1.4.3 adds that very thin or unusual fonts may still be hard to read at the lower ratio. If the type is a hairline display face, do not use the large-text exception to excuse a 3:1 pair.
- Sample gradients in more than one place. The criterion is about the background behind the text. On a gradient, check the lightest and darkest region the glyphs can occupy. Passing at the top of a hero does not cover the line that sits on the lighter stop.
- Record the decision. "Decorative only," "large text only," or "text color is the 800 step" belongs in the token docs. The next person will otherwise put
primary-500on white because the swatch looks dark.
Two worked examples, using that formula:
#767676on#FFFFFFis 4.54:1. It passes AA for normal text and fails AAA. It is not "about 4.5:1 and then it fails on an uncalibrated monitor." The ratio is a property of the code values. A dim monitor can make the pair look worse. It does not change the 4.54:1 the checker returns. The same gray on black is 4.62:1, also AA and not AAA.#6D39ACon white is 7.42:1, which passes AAA. On black it is 2.83:1, which fails even the 3:1 large-text bar. A lighter purple,#7C3AED, is 5.70:1 on white (AA, not AAA) and 3.69:1 on black (large text only). "Our purple is dark enough" is not a check. The pair is the check.
Logotypes are exempt from 1.4.3 and 1.4.6. Text that is not part of the logo is not. A wordmark can be low contrast. The button next to it cannot.
Why brand palettes fail contrast checks
Brand guidelines usually specify one primary, one secondary, and an accent. Marketing picks colors for emotion. Accessibility needs colors for legibility. Those goals overlap sometimes, not always.
Common failure patterns:
- Light gray on white.
#767676on#FFFFFFis 4.54:1. It passes AA and fails AAA. Teams often lighten it further for a "subtle" label and drop under 4.5:1 without noticing. - Brand color as text on brand-tinted backgrounds.
primary-500text onprimary-50often fails because both swatches share similar luminance. - Gradients behind copy. The ratio changes per pixel. You might pass at the top of the hero and fail where the gradient darkens.
The fix is rarely "pick a new brand color." Usually you need a darker text variant, a solid card behind copy, or a dedicated accessible step in your scale.
How to test a single foreground/background pair
Start with the pairs you ship today: body text on page background, links on default surface, button label on button fill.
- Grab the exact HEX codes from your design tokens (not the swatch you remember).
- Plug them into a contrast checker.
- Read the ratio and the AA/AAA pass/fail badges for normal and large text.
- If AA fails, try the suggested lighter/darker variant before redesigning the whole system.
On Color Mapper, the checker also shows a live text preview so you are not interpreting a ratio number in isolation.
How to audit an entire palette at once
One passing pair is not enough. Buttons, alerts, badges, and sidebar states mix colors constantly.
Use palette matrix mode: paste every HEX in your system (comma-separated) and scan the grid. Any cell below 4.5:1 is a combination you should not use for text without a redesign.
Flag these explicitly in your design system docs: "decorative only" or "requires white label on solid fill."
When contrast passes but the UI still breaks
Contrast ratio does not cover color vision deficiency. Red/green success and error states can both pass WCAG on white and still be indistinguishable for someone with deuteranopia.
Preview your palette under protanopia, deuteranopia, and tritanopia simulation. Pair that with icons, labels, or patterns. Never rely on hue alone for state.
Building accessible variants without a rebrand
When a brand hex fails on the background you ship, you do not need a new brand. You need a step on the ramp that passes. #6D39AC already passes AAA on white (7.42:1) and fails on black (2.83:1), so the fix might be a lighter label in dark mode rather than a darker purple in light mode.
The tint and shade generator builds a Tailwind-style 50–950 scale from one HEX. Each step shows contrast against white and black. Pick 700 or 800 for text, keep 500 for fills, document both in your tokens.
Workflow that holds up in review:
- Generate harmonies in the palette generator.
- Matrix-check all swatches in the contrast checker.
- Export CSS variables or Tailwind config from passing steps.
- Hand print specs through the HEX to CMYK converter when packaging joins the project.
FAQ
Did WCAG 2.2 change the contrast ratios?
No. Success Criteria 1.4.3 (4.5:1 for normal text, 3:1 for large text), 1.4.6 (7:1 and 4.5:1), and 1.4.11 (3:1 for non-text contrast) are the same in WCAG 2.1 and WCAG 2.2.
What counts as large text?
WCAG defines large-scale text as at least 18 point, or at least 14 point bold. Technique G18 equates those to about 24px and about 18.5px, because 1pt is 1.333 CSS pixels. Very thin fonts can still be hard to read at the lower ratio. The understanding doc for 1.4.3 says so directly.
Does WCAG contrast apply to logos?
Text that is part of a logo or brand name is exempt from 1.4.3 and 1.4.6. The criteria say that in the "Logotypes" exception. Text outside the logo is not exempt.
Is 4.5:1 enough for placeholder text?
Placeholder text is still text if a person has to read it. Treat it like body copy, or put a visible label outside the field.
Can I use opacity to fix contrast?
Opacity changes the color that is actually painted. Check the computed color against the real background. Do not estimate from the base hex and a slider.
What about dark mode?
Re-run the pairs for the dark theme. #6D39AC passes AAA on white at 7.42:1 and fails on black at 2.83:1. A pair that passes on white often fails on a near-black surface.
Run your current tokens through the contrast checker now. If a pair is under 4.5:1, you already know what to fix before the next accessibility audit.