Close
Color Contrast WCAG Compliance: Designer's Guide to Accessible Colors

Color Contrast: Making Text Readable for Everyone

Skill level: Beginner to Intermediate | Time: 15-20 minutes
WCAG Criteria: 1.4.3 Contrast (Minimum) AA • 1.4.1 Use of Color A • 1.4.11 Non-text Contrast AA

Our accessibility audit of 78 NGI-funded projects found 1,431 color contrast violations across 75 projects — making it the second most common accessibility failure. Nearly every single project had at least one instance of text that doesn't meet minimum contrast requirements.

The good news: contrast issues are among the simplest to fix. You don't need to restructure your markup or rewrite JavaScript. In most cases, you're changing a single hex value in your CSS. This guide gives you the knowledge and the specific color values to do that correctly.

1. Understanding Contrast Ratios

A contrast ratio measures the difference in perceived brightness between two colors. The scale runs from 1:1 (no contrast — white on white) to 21:1 (maximum contrast — black on white). WCAG Level AA defines three thresholds:

Element Type Minimum Ratio WCAG Criterion
Normal text (under 18pt, or under 14pt bold) 4.5:1 1.4.3
Large text (18pt+ regular, or 14pt+ bold) 3:1 1.4.3
UI components & graphical objects (icons, borders, focus rings) 3:1 1.4.11
18pt = 24px, 14pt bold = approximately 18.66px bold. If you're unsure whether your text qualifies as "large," assume it doesn't and aim for 4.5:1. That way you're safe regardless.

How the Ratio Is Calculated

The ratio is based on relative luminance, not just how "dark" a color looks to you. Each RGB channel is linearized (removing gamma correction) and weighted according to human vision sensitivity:

// Linearize each sRGB channel (0-255 → 0-1)
sRGB = channel / 255
linear = sRGB <= 0.04045
    ? sRGB / 12.92
    : ((sRGB + 0.055) / 1.055) ^ 2.4

// Relative luminance
L = 0.2126 * R_linear + 0.7152 * G_linear + 0.0722 * B_linear

// Contrast ratio (L1 is the lighter color)
ratio = (L1 + 0.05) / (L2 + 0.05)

Green contributes the most to perceived brightness (71.5%), which is why light green text on white fails contrast even when the hex values look "different enough." You don't need to memorize this formula — tools calculate it for you. But understanding why some color combinations feel readable yet still fail helps avoid frustration when a tool rejects your design.

2. Common Problems Found in Our Audit

These are the patterns we saw repeatedly across NGI projects. Each one includes a visual sample so you can see the problem and the fix side by side.

2.1 Light Gray Text on White Backgrounds

The single most common violation. Designers reach for gray to create visual hierarchy, but go too light.

✗ Fail — #999999 on white (ratio: 2.85:1)
This text uses #999 on white. It looks subtle but fails WCAG AA for normal text.
✓ Pass — #595959 on white (ratio: 7.0:1)
This text uses #595959 on white. Still softer than pure black, but fully compliant.
✓ Pass — #767676 on white (ratio: 4.54:1)
The lightest gray that passes 4.5:1 on pure white. Use this as your absolute floor.

2.2 Link Colors with Insufficient Contrast

Links need to meet two contrast requirements simultaneously: 4.5:1 against the background and 3:1 against surrounding text (or use an underline, which is the simpler solution).

✗ Fail — #82b1ff link on white (ratio: 2.17:1)
Here is some text with a light blue link that fails contrast.
✓ Pass — #0056b3 link on white (ratio: 7.04:1)
Here is some text with a properly contrasting link that passes.

2.3 Focus Indicators with Insufficient Contrast

Focus outlines must have at least 3:1 contrast against the surrounding background. Many sites use thin, faint outlines that vanish on light backgrounds.

✗ Fail — light blue focus ring on white
/* Barely visible focus ring */
:focus {
    outline: 2px solid #a0cfff; /* 1.63:1 vs white */
}
✓ Pass — dark blue focus ring
/* Clearly visible focus ring */
:focus {
    outline: 2px solid #0056b3; /* 7.04:1 vs white */
    outline-offset: 2px;
}

/* Even better: double ring for any background */
:focus-visible {
    outline: 2px solid #0056b3;
    outline-offset: 2px;
    box-shadow: 0 0 0 4px rgba(0, 86, 179, 0.25);
}

2.4 Placeholder Text Too Faint

Placeholder text isn't required to meet contrast minimums per WCAG (since it's not "real" content). But if users rely on it for instructions, making it unreadable defeats the purpose. Aim for at least 4.5:1.

✗ Too faint — default browser placeholders often fail
/* Many browsers default to ~#a9a9a9 (2.32:1) */
::placeholder {
    color: #c0c0c0; /* 1.6:1 on white */
}
✓ Readable placeholder
::placeholder {
    color: #767676; /* 4.54:1 on white */
    opacity: 1;     /* Firefox reduces opacity by default */
}

2.5 Dark Theme Issues

Dark themes introduce the inverse problem: text that's too dim against dark backgrounds, or accent colors that looked fine on white but wash out on dark surfaces.

✗ Fail — #707070 on #1a1a2e (ratio: 3.44:1)
Gray body text on dark background. Reads fine on a high-end monitor, fails on cheaper screens and in sunlight.
✓ Pass — #b8b8b8 on #1a1a2e (ratio: 8.6:1)
Lighter gray that comfortably meets the 4.5:1 threshold on dark backgrounds.

2.6 Gradients and Image Backgrounds

Text over gradients or images is risky because the contrast changes across the element. Text might pass at the top-left but fail at the bottom-right.

✗ Text directly on a gradient — contrast varies unpredictably
White text over a gradient. Readable here, but fails where the gradient becomes lighter.
✓ Semi-transparent overlay ensures minimum contrast
Text with a dark overlay behind it. Contrast is guaranteed regardless of the gradient position.

3. Implementation Guide

3a. Accessible Text Color Scale

Here's a tested gray scale for white backgrounds. Every value meets 4.5:1 for normal text.

Use Case Color Hex Ratio on #fff
Headings / Primary text #1a1a1a 17.4:1
Body text #333333 12.63:1
Secondary text #595959 7.0:1
Minimum for body text #767676 4.54:1
Large text only (18pt+) #949494 3.03:1
Decorative / non-text only #aaaaaa 2.32:1

3b. Link Styling with Sufficient Contrast

/* Links on white backgrounds */
a {
    color: #0056b3;              /* 7.04:1 on white */
    text-decoration: underline;  /* Always underline so color isn't the only cue */
}

a:hover {
    color: #003d7a;              /* 10.78:1 on white */
}

a:visited {
    color: #5b2d8e;              /* 9.5:1 on white */
}

a:focus-visible {
    outline: 2px solid #0056b3;
    outline-offset: 2px;
}

/* If your design requires removing underlines,
   you MUST have 3:1 contrast between link and body text
   AND provide a non-color hover/focus indicator */
a.no-underline {
    text-decoration: none;
    border-bottom: 2px solid transparent;
}
a.no-underline:hover,
a.no-underline:focus {
    border-bottom-color: currentColor;
}

3c. Focus Indicator Styling

/* Base focus style for all interactive elements */
:focus-visible {
    outline: 2px solid #0056b3;
    outline-offset: 2px;
}

/* High-contrast focus for dark backgrounds */
.dark-section :focus-visible {
    outline: 2px solid #ffd166;  /* Yellow stands out on dark BGs */
    outline-offset: 2px;
}

/* Never do this: */
/* :focus { outline: none; }     ← removes keyboard navigation */

/* If you must customize, always replace with something visible: */
button:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px #0056b3;
    border-radius: 4px;
}

3d. Button States

/* Primary button - all states maintain contrast */
.btn-primary {
    background-color: #0056b3;
    color: #ffffff;              /* 7.04:1 */
    border: 2px solid #0056b3;
    padding: 10px 20px;
    font-size: 16px;
    cursor: pointer;
    border-radius: 4px;
}

.btn-primary:hover {
    background-color: #003d7a;
    border-color: #003d7a;
    color: #ffffff;              /* 10.78:1 */
}

.btn-primary:active {
    background-color: #002a57;
    border-color: #002a57;
    color: #ffffff;              /* 14.3:1 */
}

/* Disabled buttons: lower contrast is acceptable (not interactive)
   but add visual cues beyond just color */
.btn-primary:disabled {
    background-color: #a0a0a0;
    border-color: #a0a0a0;
    color: #ffffff;
    cursor: not-allowed;
    opacity: 0.7;                /* Additional visual cue */
}

/* Secondary / outline button */
.btn-outline {
    background-color: transparent;
    color: #0056b3;              /* 7.04:1 on white */
    border: 2px solid #0056b3;   /* 3:1+ border contrast */
    padding: 10px 20px;
    cursor: pointer;
}

.btn-outline:hover {
    background-color: #0056b3;
    color: #ffffff;
}

3e. Dark Theme Color Palette

:root {
    /* Light theme */
    --bg-primary: #ffffff;
    --bg-secondary: #f5f5f5;
    --text-primary: #1a1a1a;     /* 17.4:1 on white */
    --text-secondary: #595959;   /* 7.0:1 on white */
    --link-color: #0056b3;       /* 7.04:1 on white */
    --border-color: #767676;     /* 4.54:1 on white */
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg-primary: #121212;
        --bg-secondary: #1e1e1e;
        --text-primary: #e0e0e0;     /* 14.19:1 on #121212 */
        --text-secondary: #b0b0b0;   /* 8.64:1 on #121212 */
        --link-color: #6fb3ff;       /* 8.54:1 on #121212 */
        --border-color: #888888;     /* 5.28:1 on #121212 */
    }
}

body {
    background-color: var(--bg-primary);
    color: var(--text-primary);
}

a {
    color: var(--link-color);
}

3f. Handling Gradients and Image Backgrounds

/* Technique 1: Semi-transparent overlay */
.hero-section {
    position: relative;
    background-image: url('hero.jpg');
    background-size: cover;
}

.hero-section::before {
    content: '';
    position: absolute;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.6);  /* Dark overlay */
}

.hero-section .content {
    position: relative;  /* Sit above the overlay */
    color: #ffffff;
}

/* Technique 2: Text container with solid/semi-solid background */
.hero-section .text-box {
    background-color: rgba(0, 0, 0, 0.75);
    padding: 24px;
    border-radius: 8px;
    color: #ffffff;
}

/* Technique 3: Text shadow as fallback (not sufficient alone) */
.image-caption {
    color: #ffffff;
    text-shadow:
        0 1px 3px rgba(0, 0, 0, 0.8),
        0 0 8px rgba(0, 0, 0, 0.6);
    /* Better than nothing, but use overlays for critical text */
}

4. Interactive Contrast Checker

Live Contrast Ratio Calculator

Preset problem combos from our audit:

Large Text Sample (18pt+)
Normal body text at standard size. Does this feel readable to you?

Ratio: 4.54:1

Normal Text (4.5:1)
PASS
Large Text (3:1)
PASS
UI Components (3:1)
PASS

5. Common Accessible Color Palettes

Copy-paste ready palettes tested against white (#ffffff) backgrounds.

Blues

SwatchHexRatio on #fffUse for
#003d7a10.78:1Headings, primary actions
#0056b37.04:1Links, buttons
#0073e64.63:1Borders (just passes normal text)

Greens

SwatchHexRatio on #fffUse for
#1a7a2e5.46:1Success text, passing indicators
#0d65207.24:1Body text in success contexts
#28a7453.0:1Large text or icons only

Reds / Errors

SwatchHexRatio on #fffUse for
#b51a2b6.67:1Error messages
#8b000010.01:1Critical warnings
#e74c3c3.82:1Large text or icons only
Never rely on color alone to convey meaning. Error states need an icon or text label alongside the red. Success states need more than just green. This is WCAG 1.4.1 (Use of Color). About 8% of men have some form of color vision deficiency.

6. Testing Checklist

  • All body text achieves at least 4.5:1 contrast against its background
  • All heading text (if under 18pt/24px) achieves 4.5:1
  • Links are underlined OR have 3:1 contrast against surrounding body text
  • Focus indicators have at least 3:1 contrast against the adjacent background
  • Form input borders have 3:1 contrast against the background
  • Placeholder text is readable (ideally 4.5:1, at minimum supplemented by labels)
  • Button text meets 4.5:1 in all states: default, hover, active, focus
  • Icons and graphical indicators meet 3:1 (WCAG 1.4.11)
  • Dark mode / alternate themes tested separately
  • Text over images or gradients has an overlay or container ensuring minimum contrast
  • Disabled elements are visually distinct through more than just color (opacity, pattern, cursor)
  • Error, warning, and success messages don't rely solely on color

7. Testing Tools

Tool Type Best For
WebAIM Contrast Checker Web Quick checks on individual color pairs
Colour Contrast Analyser (CCA) Desktop app Eyedropper tool for checking rendered pages, including gradients
Chrome DevTools Built-in Inspect element → color picker shows contrast ratio inline
WAVE Browser extension Full-page contrast scan alongside other accessibility checks
axe DevTools Browser extension Automated testing; identifies specific failing elements
Stark (Figma) Design plugin Check contrast during the design phase, before code is written
Pro tip: Chrome DevTools has a built-in contrast check. Inspect any text element, click the color swatch in the Styles panel, and it shows the contrast ratio with a pass/fail indicator right there. No extension needed.

8. WCAG References


Guide #3 in the NGI Accessibility Series. Based on audit data from 78 NGI-funded projects at ngi.aimsites.nl.
Related guides: Accessible Dropdown Menus

Leave a Reply