Skip to content
  • FG-94
  • 12 min read
  • Last checked November 7, 2026

How to Fix Color Contrast and Text Readability Failures on Your Website

An audit flagged low contrast or text that breaks when zoomed. Learn how contrast is calculated, how to measure it, and how to fix brand colors, overlays and global styles.

Run the health check

The audit report says “Elements must meet minimum color contrast ratio thresholds”, or Lighthouse flags “Background and foreground colors do not have a sufficient contrast ratio”. Maybe a visitor told you they cannot read your light gray body text, or that the white heading on your hero photo disappears on their phone. Or a tester zoomed your site to 200 percent and found the menu overlapping the logo and card text cut off mid-sentence.

These are readability failures. Contrast problems affect people with low vision and color vision deficiencies, older visitors, and anyone reading a phone in sunlight. Resize and reflow problems affect people who zoom or enlarge text to read at all. They are among the most frequently reported issues in automated scans, partly because they are easy for a tool to detect, and they are usually fixed at the design system level: a few global colors and layout rules, not hundreds of individual pages.

What is happening

WCAG measures contrast as a ratio between the relative luminance of two colors. Relative luminance is a number from 0 (black) to 1 (white) describing how bright a color appears to the eye. It weights green most heavily and blue least, because human vision is most sensitive to green. The contrast ratio is (L1 + 0.05) / (L2 + 0.05), where L1 is the luminance of the lighter color and L2 the darker one. Ratios run from 1:1 (identical colors) to 21:1 (black on white). Hue plays no direct part: two very different colors with similar brightness have poor contrast.

Five WCAG 2.2 Level AA success criteria cover readability:

Criterion Requirement Common failures
1.4.3 Contrast (Minimum) Text at least 4.5:1 against its background; large text at least 3:1. Large means at least 18 point (24 CSS pixels) or 14 point bold (about 18.66 CSS pixels) Light gray body text, white text on bright brand colors, text on photos, placeholder text
1.4.11 Non-text Contrast Visual information needed to identify interface components and their states, and meaningful parts of graphics, at least 3:1 against adjacent colors Pale input borders, faint focus rings, icon-only buttons, chart lines
1.4.4 Resize Text Text can be resized up to 200 percent without loss of content or function Text clipped inside fixed-height boxes, overlapping headers
1.4.10 Reflow Content works at a width of 320 CSS pixels (1280 pixels wide at 400 percent zoom) without scrolling in two directions Fixed-width containers, wide tables and menus that force sideways scrolling
1.4.12 Text Spacing Nothing breaks when a user sets line height to 1.5, paragraph spacing to 2 times the font size, letter spacing to 0.12 and word spacing to 0.16 times the font size Buttons and cards with fixed heights and hidden overflow

1.4.3 has three exemptions worth knowing: text that is part of a logo or brand name, text in an inactive (disabled) control, and pure decoration or text nobody is meant to read. These do not cover body text in a brand color, or an active button that merely looks disabled.

Before you start

You need a desktop browser with developer tools, access to your theme or page builder global color settings, and a list of your brand colors as hex values. A contrast checker helps: the Design Field Kit on this site includes one that shows the ratio and pass or fail for normal text, large text and interface components. Change global colors on a staging copy first, because they ripple through every page.

How to run the commands in this guide. All commands here are JavaScript you paste into the browser developer tools Console tab (F12, or Command+Option+I on a Mac) with your site open. They only change what you see in your own browser tab, and a reload undoes them. The theme.json example is a file in your theme folder that you edit, ideally in a child theme.

Step 1: Measure the contrast you actually have

Automated scan

Run axe DevTools or Lighthouse on your main templates: home, a service page, a blog post, the contact page and, if you sell online, product, cart and checkout. Each color-contrast failure lists the element, its foreground and background colors, the measured ratio and the required ratio. Note any results marked “needs review”: these are usually text over images or gradients, where the tool cannot determine the background, and you must check them by eye.

Devtools color picker

In Chrome or Edge, inspect a text element, then click the small color swatch beside its color property in the Styles pane. The picker shows a “Contrast ratio” value with AA and AAA lines drawn across the color field; any shade on the passing side of the AA line meets the minimum. In Firefox, the Accessibility panel has a “Check for issues” menu with a Contrast option that lists every failing text node on the page.

Check a pair by hand

When you want to test colors before they exist on the page, paste this function into the Console:

function contrast(a, b) {
  function lum(hex) {
    var c = [1, 3, 5].map(function (i) { return parseInt(hex.substr(i, 2), 16) / 255; })
      .map(function (v) { return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); });
    return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
  }
  var l1 = lum(a), l2 = lum(b);
  return ((Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)).toFixed(2);
}
contrast("#TEXTCOLOR", "#BGCOLOR");

Replace #TEXTCOLOR and #BGCOLOR with full six-digit hex colors including the hash, for example contrast("#767676", "#FFFFFF"). The function applies the WCAG relative luminance formula to each color and returns the ratio. For that example the good output is "4.54", which passes 4.5:1 for normal text. Try "#777777" instead and you get "4.48", which fails: the difference between pass and fail can be one shade. Output of "NaN" means a color was not in the six-digit format.

Step 2: Fix brand colors that fail

The most common real-world failure is a bright brand color used for text or as a button background with white text. Take a typical vivid orange, #FF6600. White text on it measures 2.94:1. That fails even the 3:1 large text threshold. You do not need a new brand to fix it. You need roles.

  • Keep the bright color for fills and decoration: large shapes, illustrations, borders next to dark text, icons that sit beside a text label.
  • Create a darker text-safe variant with the same hue. Lowering lightness to #C24E00 gives 4.79:1 with white, which passes for normal text, and still reads as the brand orange.
  • Or flip the text color. Black on #FF6600 measures 7.15:1 and near-black #1A1A1A gives 5.93:1. Dark text on a bright button is often the more faithful option.

Adjust lightness, not hue or saturation, in small steps, checking the ratio each time. Pick colors in a perceptual color model (OKLCH in modern color pickers, or HSL as a rough guide) so the darker variant still looks related. Then document each approved text and background pair, not just a list of swatches, so nobody later combines two colors that each pass with white but fail with each other.

Links inside paragraphs need extra care. If a link is distinguished from surrounding text by color alone, WCAG 1.4.1 Use of Color applies as well: either underline links in body text, or make sure the link color differs from the body text color by at least 3:1 and add another cue on hover and focus. Underlines are the simpler, safer choice.

Step 3: Fix text over images and gradients

Hero sections with white text over a photo pass in the designer mockup and fail in the wild, because the bright part of the photo moves when the image is cropped for a different screen size. Contrast is measured against the actual pixels behind each letter, so the worst spot counts.

To confirm: view the page at desktop, tablet and phone widths (use the device toolbar in developer tools). Use the color picker eyedropper to sample the lightest area behind the text and test that color against the text color.

Fixes, in order of reliability:

  1. Put text on a solid or near-solid panel placed over the image. This always works and never depends on the photo.
  2. Add a background overlay to the section: a dark layer, for example black at 50 to 60 percent opacity, between the image and the text. Most page builders have a background overlay setting on sections and containers. Recheck the brightest area after adding it.
  3. Choose and crop images so the text area falls on a calm, dark (or light, for dark text) part of the photo at every breakpoint. Use the builder per-device background position setting.

A text shadow alone is not a dependable fix; it helps only when it is dense enough to act as a background. Also check slider and carousel slides one by one, because each image is a different background.

Step 4: Placeholder text, input borders and disabled states

Placeholders

Default browser placeholder text is light gray and often fails 4.5:1. If the placeholder carries information (an example format, a hint), it must meet text contrast. If it is the only label for the field, that is a form labeling failure, covered in our separate guide on accessible forms. To make placeholders readable, add this to your custom CSS:

::placeholder {
  color: #PLACEHOLDER_COLOR;
  opacity: 1;
}

Replace #PLACEHOLDER_COLOR with a gray that reaches at least 4.5:1 against your field background, such as #767676 on white. opacity: 1 stops browsers that fade placeholder text from lowering the contrast further. Good result: the contrast check passes on the placeholder. Remember that placeholder text disappears when someone types, so never rely on it for instructions people need while typing.

Input borders and controls

A text field on a white page is often identified only by its border. Under 1.4.11, that border needs 3:1 against the background. The popular very light gray #CCCCCC on white measures 1.61:1, which fails; #949494 just passes at 3.03:1. The same applies to checkbox and radio outlines, toggle switches, and the current-state indicator on tabs.

Disabled states

Inactive controls are exempt from contrast requirements, which is why disabled buttons are allowed to look faded. The problem arises when something that is active looks disabled, or when a form disables its submit button until every field is valid, leaving people unable to tell why they cannot proceed. Keep active controls at full contrast and let people press submit, then show clear errors.

Step 5: Fix colors at the source: theme.json and builder globals

Fixing contrast element by element creates a site that drifts back into failure. Fix the global definitions instead.

Block themes and theme.json

In block themes, the palette and element colors live in theme.json and in the Site Editor Styles panel (user changes made there are saved in the database and override the file). For a quick change, open Appearance, Editor, Styles, Colors, and adjust Text, Background, Links and Buttons; the editor previews the result. For a durable change in a child theme, edit theme.json:

{
  "version": 3,
  "settings": {
    "color": {
      "palette": [
        { "slug": "COLOR_SLUG", "color": "#HEXVALUE", "name": "COLOR_NAME" }
      ]
    }
  },
  "styles": {
    "elements": {
      "link": { "color": { "text": "var(--wp--preset--color--COLOR_SLUG)" } }
    }
  }
}

COLOR_SLUG is a short lowercase name you choose, such as brand-text; COLOR_NAME is the label editors see in the color picker; #HEXVALUE is your tested text-safe color. WordPress turns each palette entry into a CSS custom property named --wp--preset--color-- plus the slug, and the styles.elements.link rule applies it to links site-wide. "version": 3 is the current schema. Good result: after saving and reloading, links use the new color everywhere and the color appears in the editor palette. Bad result: no change, usually because a user-level change in the Site Editor Styles panel overrides the file; check Styles and reset that color if needed. Invalid JSON (a missing comma or quote) makes WordPress ignore the file, so validate it before uploading.

Page builders

Every major builder has global colors (sometimes called a palette, global swatches or color variables). Change the failing value once in global settings and every element linked to it updates. The catch is elements that were given a hard-coded color instead of the global one: they will not update. After changing globals, rescan, and for any remaining failures, relink the element to the global color rather than typing a new hex value. Builders that support global classes let you define a text-on-brand class once and reuse it, which keeps approved pairs together.

Step 6: Test resize, reflow and text spacing (1.4.4, 1.4.10, 1.4.12)

Resize and reflow

Set your browser window to about 1280 pixels wide and zoom to 200 percent (Control or Command and the plus key), then to 400 percent. At 400 percent, the layout should behave like a phone: one column, no horizontal scrolling for normal content, the menu collapsed into a toggle that still works. Data tables, maps and diagrams may scroll sideways; ordinary text may not. Common causes of failure are fixed pixel widths on containers, white-space: nowrap on long labels, and absolutely positioned elements that overlap when text grows.

Also check the viewport meta tag in your page source. maximum-scale=1 or user-scalable=no stops people pinch-zooming on phones. Remove them; if a theme or plugin adds them, look for a setting or replace the component.

Text spacing

Paste this into the Console to apply the 1.4.12 values to the whole page:

var s = document.createElement("style");
s.textContent = "* { line-height: 1.5 !important; letter-spacing: 0.12em !important; word-spacing: 0.16em !important; } p { margin-bottom: 2em !important; }";
document.head.appendChild(s);

No placeholders. It adds a style element with the WCAG test values, using !important so they override your theme. Good result: text gets airier and longer, and boxes grow to fit. Bad result: text spills out of buttons, card text is cut off, words overlap icons, or navigation items wrap under each other. The cause is almost always a fixed height combined with overflow: hidden. Replace fixed heights with min-height or padding, and let containers grow with their content.

Advanced: the luminance math, worked through

Understanding the formula helps you predict which colors will pass before you test. Take #FF6600. Convert each channel from 0 to 255 into 0 to 1: red is 1.0, green is 102 divided by 255, or 0.4, blue is 0. Each channel is then linearized: values at or below 0.04045 are divided by 12.92, and larger values use ((value + 0.055) / 1.055) raised to the power 2.4. Red stays 1.0; green becomes about 0.133; blue stays 0. Relative luminance is 0.2126 × R + 0.7152 × G + 0.0722 × B, which here is about 0.2126 + 0.0951 = 0.308. White has luminance 1, so the ratio is (1 + 0.05) / (0.308 + 0.05), about 2.94.

Three practical lessons fall out of the math. First, green dominates: a color with a strong green channel is bright even if it looks saturated, so yellows and limes almost never work as text on white. Second, the 0.05 term represents ambient light flare, which is why even pure black on mid gray has limited contrast. Third, because the curve is non-linear, small changes in a dark color barely move the ratio, while small changes in a mid-tone move it a lot. That is why #767676 passes and #777777 fails.

You may see newer contrast models such as APCA discussed for future accessibility guidelines. They are interesting, but they are not part of WCAG 2.2. The ratio described here is the one audits and complaints measure against today. Also note that the WCAG formula assumes sRGB colors; if you use wide-gamut colors defined in oklch() or color(display-p3 ...), check the sRGB fallback your design uses.

When to contact your host

Color and layout fixes are yours or your designer, but two server issues can make it look as if your fix did not work. If you changed global colors and visitors still see the old ones, a page cache, CDN or builder-generated CSS file may be stale. Regenerate the builder CSS files first (most builders have a regenerate or clear cache button), then purge your caching plugin. If the old colors persist, open a ticket with the page URL, the stylesheet URL and its response headers from the Network tab, and the UTC time you made the change. You can inspect caching headers yourself with our diagnostic tools.

Not legal advice: this guide explains how to meet the technical criteria. If contrast findings arrived as part of a complaint or demand letter, take the legal questions to a lawyer.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides
FG-131 Accessibility

Does the European Accessibility Act Apply to Your Website?

The European Accessibility Act has applied since 28 June 2025. Work out whether your site is an in-scope service, whether the microenterprise exemption covers you, and what a practical plan looks like.

Read the guide
10 min read
FG-140 SEO

How to Fix Page Titles, Meta Descriptions and Headings in WordPress

Missing, duplicate or cut-off titles and descriptions, no H1 or two of them, and headings that skip levels. What each one does for search engines and screen readers, and how to fix it in Rank Math, Yoast and your page builder.

Read the guide
17 min read

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close