ScreenshotNeo

BlogGuides

Color Contrast Accessibility: How to Check and Improve It

Learn how to measure WCAG text and non-text contrast, identify the right color pairs, and fix failing combinations without relying on guesswork.

By the ScreenshotNeo team4 October 20269 min read

To check color contrast, compare the relative luminance of the foreground with the color immediately behind it. Under WCAG 2.2 AA, ordinary text generally needs a contrast ratio of at least 4.5:1; large-scale text generally needs 3:1. Qualifying interface components and graphical information have a separate 3:1 requirement under WCAG 2.1 SC 1.4.11, subject to exceptions. If a pair fails, change a color and measure the actual rendered pair again.

A ratio is a measurement of luminance difference, not hue difference. Blue and orange can look distinct while still having too little luminance contrast. A checker can calculate a pair you give it, but a complete review also requires finding the right pairs, states, and backgrounds in the design or page.

1. Know which contrast threshold applies

Content being checked WCAG threshold Criterion
Ordinary text and images of text At least 4.5:1 WCAG 2.2 SC 1.4.3, Level AA
Large-scale text and images of large-scale text At least 3:1 WCAG 2.2 SC 1.4.3, Level AA
Visual information needed to identify qualifying controls or graphical objects Generally at least 3:1 against adjacent color(s) WCAG 2.1 SC 1.4.11
Ordinary text at enhanced contrast Generally at least 7:1 WCAG 2.1 SC 1.4.6, Level AAA
Large-scale text at enhanced contrast Generally at least 4.5:1 WCAG 2.1 SC 1.4.6, Level AAA

WCAG defines large-scale text by size: commonly summarized as at least 18 pt regular or 14 pt bold. Use the rendered text size and the applicable criterion; being a heading or visually prominent does not by itself qualify. The thresholds are strict: a result such as 4.499:1 does not pass 4.5:1 by rounding. See W3C’s explanation of SC 1.4.3.

SC 1.4.3 has exceptions, including inactive interface components, pure decoration, content not visible to anyone, text in a picture containing significant other visual content, and logotypes. SC 1.4.11 concerns visual information needed to identify user-interface components and graphical objects; it does not mean every decorative image or every pixel must pass 3:1. Check the criterion wording for the exact case.

2. Identify the real foreground and background

Before opening a checker, list what needs checking. For text, compare the text color to the color immediately behind the glyphs. For a meaningful icon, control boundary, or graphical indicator, identify the adjacent colors needed to perceive or identify it. Include normal, hover, focus, selected, disabled, and error states where applicable.

  • Design tokens: use the actual foreground and background token values from the design system.
  • CSS: inspect computed styles in the browser, especially when variables, inheritance, opacity, or themes change the final values.
  • Rendered page: use an on-screen sampling tool when the displayed color comes from an image, gradient, blend, or overlay.

Gradients and images may put different colors behind different parts of the same text. A single pair of HEX values cannot represent all such locations. Inspect the least favorable relevant area and check the rendered presentation. Unspecified foreground or background colors can also make an isolated text color impossible to assess reliably. W3C describes the expected pair and calculation in Technique G18.

3. Calculate the ratio from HEX colors

A contrast checker is usually the fastest option for routine work. If you need to validate a value, the following runnable Python script computes WCAG relative luminance from sRGB HEX colors and prints the ratio. It uses only the Python standard library.

#!/usr/bin/env python3
import sys

def srgb_channel_to_linear(value):
    channel = value / 255.0
    if channel <= 0.04045:
        return channel / 12.92
    return ((channel + 0.055) / 1.055) ** 2.4

def parse_hex(value):
    value = value.strip().lstrip("#")
    if len(value) == 3:
        value = "".join(ch * 2 for ch in value)
    if len(value) != 6:
        raise ValueError("Expected #RGB or #RRGGBB")
    try:
        return tuple(int(value[i:i + 2], 16) for i in (0, 2, 4))
    except ValueError as exc:
        raise ValueError("Color must contain hexadecimal digits") from exc

def luminance(color):
    red, green, blue = (srgb_channel_to_linear(c) for c in color)
    return 0.2126 * red + 0.7152 * green + 0.0722 * blue

def contrast_ratio(first, second):
    lighter, darker = sorted((luminance(first), luminance(second)), reverse=True)
    return (lighter + 0.05) / (darker + 0.05)

if len(sys.argv) != 3:
    raise SystemExit(f"Usage: {sys.argv[0]} FOREGROUND_HEX BACKGROUND_HEX")
try:
    foreground = parse_hex(sys.argv[1])
    background = parse_hex(sys.argv[2])
except ValueError as exc:
    raise SystemExit(str(exc))

ratio = contrast_ratio(foreground, background)
print(f"Contrast ratio: {ratio:.4f}:1")
print(f"Ordinary text AA (4.5:1): {'PASS' if ratio >= 4.5 else 'FAIL'}")
print(f"Large text AA (3:1): {'PASS' if ratio >= 3 else 'FAIL'}")

Save as contrast.py and run python3 contrast.py '#333333' '#ffffff'. The script handles short and long opaque sRGB HEX notation. It deliberately does not parse CSS colors such as rgb(), hsl(), alpha transparency, gradients, or images; resolve those to the actual rendered opaque colors before using it. Its printed pass results are limited to the two text thresholds and do not determine whether a non-text criterion applies.

The calculation uses (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter relative luminance and L2 the darker. Each sRGB channel is linearized before luminance is weighted as 0.2126 red + 0.7152 green + 0.0722 blue. The ratio is symmetric, so swapping the two colors produces the same result. Full calculation details are in W3C Technique G18.

4. Use a checker in your design or page workflow

  1. Choose the exact foreground and background, or sample them from the rendered page.
  2. Select the applicable threshold based on text size or whether the item is qualifying non-text content.
  3. Check the normal presentation and other meaningful states, including focus and hover where they change the colors.
  4. For gradients, translucent overlays, or image backgrounds, inspect the rendered regions rather than trusting one token pair.
  5. Record the failing pair and the token or component that owns it so a fix can be applied consistently.

If the design is in Figma, its Color Contrast Checker documents HEX input, AA/AAA feedback, color simulation, a swap control, and palette adjustment into Figma Design. The W3C evaluation tools list catalogs contrast-related tools and states that listing does not mean W3C endorses them. WAI’s Easy Checks provides context for a first review. No checker replaces deciding which visual pairs and states matter.

5. Improve a failing color pair

  1. Choose a color to change. Darken the darker color, lighten the lighter color, or select a different pair. Preserve the intended meaning of colors such as error and success states.
  2. Make a deliberate adjustment. Small RGB changes may have little effect on luminance. If the ratio is far below threshold, increase the light-dark separation rather than only shifting hue.
  3. Recalculate the pair. Check the exact resulting color against the correct threshold without rounding up.
  4. Review the real component. Recheck all affected text, icons, borders, backgrounds, and states in the rendered page or design.
  5. Check neighboring combinations. A shared token change can fix one component and break another. Search usages of the token, then repeat checks for each relevant foreground/background pair.

Contrast adjustments can affect brand appearance and visual hierarchy. Prefer a shared, accessible token where it fits; use a component-specific token when a global change would create other failures. For controls, do not assume text passing 4.5:1 means the boundary or state indicator also passes its separate non-text requirement.

6. Common mistakes and edge cases

  • Judging by hue: colors that look different can have similar luminance. Measure the ratio.
  • Checking only body copy: headings, labels, placeholder text, icons, control boundaries, and meaningful chart elements may need review under the relevant criterion.
  • Calling text large because it is prominent: use WCAG’s defined large-scale size, not the role or visual importance of the text.
  • Checking source colors instead of rendered colors: opacity, overlays, inherited styles, dark mode, and gradients can change the actual pair.
  • Using one background value for a photo or gradient: check the actual regions behind the content, particularly where contrast is weakest.
  • Rounding up a near miss: values below a minimum fail even when displayed to fewer decimal places as the threshold.
  • Treating the ratio as a full accessibility audit: a pair checker does not decide whether the right item was assessed or whether the page is usable by keyboard, screen reader, or in other conditions.

7. Troubleshooting

Symptom Likely cause Fix
Checker reports a ratio much lower than expected Foreground or background value differs from the rendered color, possibly due to opacity, inheritance, or theme styles. Inspect computed styles and sample the rendered page. Resolve transparency against the actual background before calculating.
The ratio passes for a button label but the button is still hard to find Only text contrast was checked; the control boundary or visual state may be the relevant non-text information. Assess qualifying control information separately under SC 1.4.11 against adjacent colors.
A checker accepts colors but not CSS notation The tool expects HEX values rather than CSS declarations. Read the computed color value and convert or enter the actual color in a supported format.
A value shows 4.5 after rounding but fails The exact value is below 4.5:1. Increase contrast until the unrounded result meets or exceeds the threshold.
Text over an image passes in one spot and fails elsewhere The background varies across the text. Check multiple rendered locations and improve the worst affected region, for example by adjusting the image treatment or text backing.
A palette change fixes one component and breaks another The changed token is shared by combinations with different contrast needs. Review all token usages and use a component-specific value if a shared adjustment cannot satisfy each pair.

8. Automate checks carefully

For a larger site, treat automated contrast checks as a way to find candidate problems, then review context. A tool may inspect CSS declarations or rendered pixels, but dynamic states, pseudo-elements, images, gradients, overlays, and meaningful non-text scope can complicate what to compare. Keep color tokens and component states explicit, and include representative pages and themes in your review workflow.

For page review, capture representative states consistently so a human can inspect the actual layout and colors. ScreenshotNeo is a website screenshot API and MCP server by Yorker Media. Its captures can help document a rendered page for review, but a screenshot itself does not calculate WCAG contrast or establish conformance. See ScreenshotNeo and its documentation.

Or skip the browser setup

Use ScreenshotNeo to capture a representative page for visual review with one GET request:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same endpoint can be called from Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Or from Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

Read the ScreenshotNeo API documentation for request parameters and response details. Cookie banners are accepted and removed before capture, along with known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. A screenshot is a review aid: use a contrast calculator on the identified colors to determine the ratio. Sign up for 1,000 free screenshots a month, with no card.

9. Performance, reliability, and cost notes

For individual color pairs, calculation is trivial; the time-consuming work is locating actual pairs and validating states. A local token audit is fast and repeatable, while rendered-page review is needed when styles or imagery alter the final colors. Keep screenshots representative and avoid treating a single page state as a site-wide check.

ScreenshotNeo offers caching with a chosen TTL, asynchronous jobs with signed webhooks, and bulk capture for up to 100 URLs per call. Its pricing is Free for 1,000 shots/month, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free. Every feature is on every plan. Use captures where visual documentation is useful, and perform contrast measurements separately with the correct colors.

10. FAQ

Does a contrast pass mean a page is accessible?

No. It answers a specific color-pair question. Accessibility review also covers content, semantics, keyboard behavior, focus, and other requirements.

Do I need to check every decorative image?

Not automatically. SC 1.4.11 applies to qualifying visual information needed to identify controls and graphical objects, with exceptions. Determine whether the image conveys information or is purely decorative.

Can I use a checker’s suggested color without further review?

Use it as a candidate, then check affected components, states, and combinations in the actual design or page.

Does ScreenshotNeo replace a contrast checker?

No. It captures webpages for visual inspection; it does not calculate contrast ratios or certify WCAG conformance.