ToolKitSphere IconToolKitSphere
Color Tools

How to Fix Failing Color Contrast on Your Site

Online Tools Platform Team8 min read

An accessibility report landing in your inbox with forty contrast failures feels like a palette rewrite. It almost never is. Most failing sites have a handful of root causes — one gray that is too light, one brand color used for both large and small text, one border token that was chosen for elegance — repeated across hundreds of components. Fix the root color and the report collapses.

This post is the repair process: how to triage failures, how to adjust a color so it passes without looking like a different color, and how to stop the same failures coming back. For the underlying theory — how relative luminance produces the ratio, and why hue barely affects it — see The Complete Guide to Accessible Color and Contrast.

Step 1: Inventory the colors, not the components

Working component by component is the slowest possible approach, because the same three or four colors are behind most of the failures. Start by extracting the actual color values from your stylesheets and deduplicating them.

Paste your CSS into the CSS Color Code Extractor & Analyzer and you get a deduplicated list of every hex, rgb(), hsl(), and named color in the file, with usage counts. Two things usually jump out immediately:

  • Near-duplicates. #767676, #777, and #757575 all present, doing the same job. Consolidating them reduces the number of colors you need to fix and prevents one variant being repaired while its twins survive.
  • Colors with no token. Hard-coded values that bypassed the design system entirely. These are where the worst failures live, because nobody checked them.

The analyzer runs entirely in the browser, so pasting a production stylesheet — or an unreleased one — sends nothing to a server.

Step 2: Triage by what the color is used for

Not every failure costs the same to fix or matters the same amount. Sort them into four buckets before touching anything:

Body text below 4.5:1. Highest priority. This is the text every user reads, and it is a Level AA failure under 1.4.3.

Small UI labels, captions, placeholders, helper text. Same 4.5:1 requirement and frequently worse offenders than body copy, because designers deliberately de-emphasize them. Placeholder text is not decorative — it conveys information and must pass.

Large text and headings below 3:1. Lower requirement, so many "failures" here are near misses that a small nudge fixes.

Borders, icons, focus rings below 3:1. These fall under 1.4.11 Non-text Contrast, and they are the bucket teams most often skip. A 1px #E5E5E5 input border on white measures about 1.3:1 — clean-looking and functionally invisible.

Fix in that order. Body and label text first delivers most of the real-world benefit, and often clears half the report because a single text-secondary token was behind all of it.

Step 3: Shift lightness, keep the hue

Here is the core technique, and it is the reason contrast repair rarely damages a brand.

The contrast ratio is computed from relative luminance, which is a weighted sum of the linearized red, green, and blue channels. Hue affects that sum only indirectly. Saturation barely moves it. Lightness moves it a lot. So the fix is to convert the color to HSL, hold H and S fixed, and step L.

Worked example. A brand blue #4A90D9 on white measures 3.0:1 — a clear fail for body text. In HSL that is hsl(210, 64%, 57%).

Adjustment Hex Ratio on white Verdict
L 57% (original) #4A90D9 3.0:1 Fails AA normal text
L 47% #2E7BCC 3.9:1 Still fails
L 42% #2C6DAE 4.6:1 Passes AA
L 33% #22558A 6.6:1 Comfortable margin
L 28% #1D4875 8.5:1 Passes AAA

Every one of those still reads as the same blue, because the hue never moved. Nobody outside the design team will notice the difference between the first and fourth rows in isolation; everyone with low vision will notice the difference in readability.

Two rules that make this work in practice:

Darken the foreground, not the background. Background surfaces carry brand weight and cover most of the screen. A 10% lightness shift on a page background changes the whole feel of the product; the same shift on a text color is invisible.

Step in small increments and measure each one. Perceived lightness and computed luminance do not move in step across hues — yellows and greens shed luminance far faster per lightness step than blues do. Check each candidate in the Contrast Ratio Checker (WCAG AA/AAA) rather than assuming a fixed percentage works everywhere.

If you would rather not step manually, the Accessible Color Pair Finder takes your failing color and target threshold and returns the nearest compliant variants directly. For building a full ramp — the 50 through 900 scale most design systems use — the Color Shade Generator produces evenly spaced steps from a base color, which you then verify pairwise against your surface colors.

Step 4: Handle the cases lightness alone won't fix

Some failures need a different move.

Text on images and gradients. There is no single background color to measure against, and the ratio must hold against the worst pixel behind the text, not the average. Add a semi-opaque scrim (a rgba(0,0,0,0.5) layer between image and text is the common choice), place the text on a solid card, or apply a text shadow heavy enough to guarantee separation. Then measure the text against the scrim color, which is now a known value.

Semi-transparent colors. An rgba() or opacity value does not have a fixed ratio — it depends on whatever is behind it. Compute the composited color first (blend the foreground with the background at the given alpha), then measure that. Wherever possible, replace transparent text colors with the equivalent solid value in your tokens.

Buttons and other multi-part components. A button generates at least three pairs: label against fill (4.5:1, or 3:1 if the label is large), fill or border against page background (3:1, criterion 1.4.11), and focus indicator against both. Passing one does not imply the others.

Interactive states. Hover, focus, active, visited, and error states each introduce new colors. A common failure pattern is a hover state that lightens a button fill just enough to drop the white label below 4.5:1.

Charts and status colors. If a "failure" is really that two series look alike, contrast is the wrong metric — that is a 1.4.1 Use of Color problem. Designing for Color Blindness: A Practical Guide covers redundant encoding, and Why You Shouldn't Rely on Color Alone covers the criterion itself.

Step 5: Fix the token, not the instance

The difference between a site that stays accessible and one that regresses in a quarter is where the fix lands.

Patch the component and you have fixed one instance. Patch the token and you have fixed every current and future use. Repair at the level of a semantic scale — text-primary, text-secondary, text-disabled, border-default, border-strong — with each token's measured ratio against each surface recorded next to its definition.

That documented ratio is what prevents the regression. When someone later proposes lightening text-secondary for visual balance, the recorded "4.7:1 on surface-default" makes the cost of the change explicit instead of invisible.

Three habits that keep it fixed:

  • Give yourself margin. Ship at 5:1 rather than 4.51:1 for small text so a future nudge or opacity does not immediately break conformance.
  • Ban raw hex in components. If a color can only come from a token, an unverified color cannot enter the codebase.
  • Re-verify dark mode separately. Contrast is symmetric, so a passing pair stays passing when inverted — but dark palettes are almost never straight inversions, and the softened grays that make dark mode comfortable are exactly the ones that slip below threshold.

What "done" looks like

You are finished with a contrast pass when every text token has a recorded ratio against every surface it is allowed to appear on, every interactive component passes in all of its states, and every border, icon, and focus ring clears 3:1. Not when a scanner reports zero errors — automated checks find contrast failures reliably and miss most other color problems entirely.

Start with the two pairs on every page: body text on the page background, and the primary button label on its fill. Measure both in the Contrast Ratio Checker (WCAG AA/AAA), fix what fails at the token level, and work outward from there.

Frequently asked questions

How do I fix a color that fails contrast without changing my brand color?

Convert the color to HSL and adjust the lightness value in small steps while keeping hue and saturation fixed. Darken the foreground on light backgrounds, lighten it on dark backgrounds. Because contrast is driven by relative luminance, a lightness shift of 10-15% usually clears the threshold while still reading as the same brand color.

Should I change the text color or the background color?

Change the text color in almost all cases. Backgrounds occupy far more area, so shifting them changes the overall feel of the page and affects every other element sitting on them. Adjusting a single text token is a smaller, safer change.

Why does my button pass contrast but still fail an audit?

Buttons create several color pairs, not one. The label needs 4.5:1 against the button fill, the fill or border needs 3:1 against the page background under criterion 1.4.11, and the hover, focus, and active states each create new pairs that must also pass.

Does disabled text need to meet contrast requirements?

No. WCAG 1.4.3 exempts text that is part of an inactive user interface component. However, the control must actually be inactive. A low-emphasis button that is still clickable is not disabled and must pass.

How do I check text placed over an image?

The ratio must hold against the lightest and darkest pixels behind the text, not the average. The standard fixes are a semi-opaque scrim between the image and the text, a solid text container, or a text shadow strong enough to guarantee separation.

Can I fix contrast by increasing font size instead of changing colors?

Sometimes. Moving text to at least 18pt (24px), or 14pt (18.66px) bold, lowers the AA requirement from 4.5:1 to 3:1. This works for headings and callouts but is rarely appropriate for body copy or small labels.

What is the fastest way to find every failing color in my CSS?

Extract the full color list from your stylesheet first, then test the pairs that actually occur together. Auditing a deduplicated palette of 20 colors is far faster than clicking through hundreds of rendered components, and it finds near-duplicate tokens worth consolidating at the same time.

Try the related tools

Related articles