color-contrast
Load this skill whenever the project contains text, UI components, icons, form controls, data visualisations, or focus indicators — in short, almost every project. Under no circumstances hard-code colour values without verifying contrast ratios. Absolutely always ensure text meets 4.5:1, large text meets 3:1, and non-text UI elements meet 3:1 against adjacent colours. Test in light mode, dark mode, and forced-colors (high contrast) mode.
Color Contrast Accessibility Skill
Canonical source:
examples/COLOR_CONTRAST_ACCESSIBILITY_BEST_PRACTICES.mdinmgifford/ACCESSIBILITY.mdThis skill is derived from that file. When in doubt, the example is authoritative.
Apply these rules whenever implementing or reviewing colour choices in HTML, CSS, SVG, or any visual interface element.
Core Mandate
Sufficient contrast between foreground and background colors is a prerequisite for users to read text, identify UI components, perceive graphical content, and track keyboard focus. Color alone must never be the sole means of conveying information.
All visual interface elements that convey information or require user interaction must meet WCAG 2.2 Level AA contrast thresholds in light mode, dark mode, and forced-colors (high contrast) mode.
Severity Scale (this skill)
| Level | Meaning |
|---|---|
| Critical | Contrast failure makes content or interaction completely inaccessible |
| Serious | Contrast failure significantly impairs access for a disability group |
| Moderate | Contrast issue degrades usability but content remains partially accessible |
| Minor | Best-practice gap; marginal impact |
Critical: Text Contrast (WCAG 1.4.3)
Normal text and images of text must meet these minimums:
| Text type | Minimum (AA) | Enhanced (AAA) |
|---|---|---|
| Normal text (below 18pt / 14pt bold) | 4.5:1 | 7:1 |
| Large text (18pt+ or 14pt+ bold) | 3:1 | 4.5:1 |
| Logotypes / purely decorative text | Exempt | Exempt |
| Disabled controls | Exempt | Exempt |
"Large text" means 18pt (≈ 24 CSS px) or larger in regular weight, or
14pt (≈ 18.67 CSS px) or larger in bold weight.
Preferred CSS pattern — text colours via custom properties
:root {
--color-text: #1a1a1a; /* contrast vs #fff: 16.75:1 ✓ */
--color-text-muted: #595959; /* contrast vs #fff: 7.0:1 ✓ */
--color-heading: #333333; /* contrast vs #fff: 12.63:1 ✓ */
--color-link: #0066cc; /* contrast vs #fff: 4.52:1 ✓ */
--color-background: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--color-text: #e8e8e8; /* contrast vs #1a1a1a: 13.61:1 ✓ */
--color-text-muted: #b0b0b0; /* contrast vs #1a1a1a: 7.0:1 ✓ */
--color-link: #66aaff; /* contrast vs #1a1a1a: 5.74:1 ✓ */
--color-background: #1a1a1a;
}
}
Avoid
/* Bad — 2.4:1 contrast, fails 1.4.3 */
.placeholder { color: #aaaaaa; }
/* Bad — informative text in low-contrast colour */
.note { color: #888; } /* fails against white background */
Serious: Non-text Contrast (WCAG 1.4.11)
UI components and graphical objects required to understand or operate the interface must have 3:1 contrast against adjacent colours.
Applies to
- Form input borders (text fields, checkboxes, radio buttons, selects)
- Interactive component boundaries (buttons without text, sliders, toggles)
- Icons and graphical objects that convey meaning
- Charts and data visualisation elements that encode information
- Status indicators (progress bars, meter fills)
Does not apply to
- Decorative graphics that convey no meaning
- Inactive / disabled components
- Logos and brand marks
- Graphical elements supplementary to adjacent text
Form control pattern
/* Checkbox border must contrast 3:1 against its background */
input[type="checkbox"] {
--checkbox-border: #767676; /* 4.54:1 against #fff ✓ */
appearance: none;
width: 1.25rem;
height: 1.25rem;
border: 2px solid var(--checkbox-border);
border-radius: 3px;
}
Serious: Use of Color (WCAG 1.4.1)
Color alone must not be the sole means of conveying information, indicating an action, prompting a response, or distinguishing a visual element. A second, non-color cue must always accompany color.
Common failure patterns
<!-- Bad: required field indicated only by red label color -->
<label style="color: red;">Email address</label>
<input type="email">
<!-- Bad: error state communicated only by red border -->
<input type="email" style="border-color: red;">
Preferred patterns
<!-- Good: required field — asterisk + color + screen-reader text -->
<label>
Email address
<span aria-hidden="true" class="required-marker">*</span>
<span class="sr-only">(required)</span>
</label>
<input type="email" aria-required="true">
<!-- Good: error state — icon + text + color + aria-invalid -->
<div class="field field--error">
<label for="email">Email address</label>
<input id="email" type="email"
aria-describedby="email-error" aria-invalid="true">
<p id="email-error" class="error-message">
<svg role="img" aria-label="Error" aria-hidden="true">
<use href="#icon-exclamation"></use>
</svg>
Please enter a valid email address.
</p>
</div>
Link distinction from surrounding text
Links within body text must be distinguishable from surrounding text by more than color alone. Use underline (the browser default) or another non-color cue.
/* Good: underline preserved */
a {
color: #0066cc;
text-decoration: underline;
}
Serious: Focus Appearance (WCAG 2.4.13)
WCAG 2.2 2.4.13 Focus Appearance (Level AA) requires visible keyboard focus indicators that:
- Enclose the focused component with an area of at least the perimeter × 2 CSS px in thickness.
- Have 3:1 contrast between focused and unfocused states.
- Have 3:1 contrast against every adjacent color in the unfocused state.
Preferred CSS pattern
:root {
--focus-ring-color: #0066cc;
--focus-ring-width: 3px;
--focus-ring-offset: 2px;
}
:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
/* White halo ensures visibility on dark backgrounds */
box-shadow: 0 0 0 calc(var(--focus-ring-width) + var(--focus-ring-offset))
#ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--focus-ring-color: #99ccff;
}
}
C40 two-color focus indicator
/* Works on both light and dark surfaces */
:focus-visible {
outline: 3px solid #000000;
outline-offset: 1px;
box-shadow: 0 0 0 5px #ffffff;
}
Avoid
/* NEVER do this — completely removes focus visibility */
:focus { outline: none; }
/* NEVER do this without providing an alternative focus style */
*:focus { outline: 0 !important; }
Serious: Forced-Colors Mode (WCAG 1.4.3, 1.4.11)
Windows High Contrast Mode and forced-colors replace author colors with
system colors. Interfaces break when CSS background-color, box-shadow, or
color properties are the sole means of conveying meaning.
Use outline for focus rings — it survives forced-colors
/* Good: outline is forced-colors-safe */
:focus-visible {
outline: 3px solid Highlight;
outline-offset: 2px;
}
/* Risk: box-shadow may not render in forced-colors mode */
:focus-visible {
box-shadow: 0 0 0 3px #0066cc; /* may be suppressed */
}
Restore lost meaning in forced-colors mode
@media (forced-colors: active) {
.button {
background-color: ButtonFace;
color: ButtonText;
border: 2px solid ButtonBorder;
}
/* Restore SVG icon visibility */
.icon {
forced-color-adjust: auto;
}
}
Testing forced-colors mode
- Enable High Contrast Mode in Windows Accessibility settings
- Chrome DevTools → Rendering → "Emulate CSS media feature forced-colors: active"
- Firefox:
about:config→ui.forcedColors: 1
Moderate: Semantic Color Token Pattern
Centralizing all design-system colors as CSS custom properties makes contrast validation and theming manageable at scale.
/* Token layer — raw values */
:root {
--color-neutral-600: #595959; /* 7.0:1 on #fff */
--color-neutral-900: #1a1a1a; /* 16.75:1 on #fff */
--color-brand-500: #0066cc; /* 4.52:1 on #fff — OK for normal text ✓ */
--color-brand-700: #004c99; /* 7.59:1 on #fff — OK for all text ✓ */
/* Semantic layer */
--color-text-primary: var(--color-neutral-900);
--color-text-secondary: var(--color-neutral-600);
--color-text-link: var(--color-brand-500);
--color-surface: #ffffff;
--color-border: #e8e8e8; /* 3:1 for non-text ✓ */
}
@media (prefers-color-scheme: dark) {
:root {
--color-text-primary: #e8e8e8;
--color-text-secondary: #a0a0a0; /* 7.11:1 on #1a1a1a ✓ */
--color-text-link: #66aaff; /* 5.74:1 on #1a1a1a ✓ */
--color-surface: #1a1a1a;
--color-border: #444444; /* 3.1:1 vs #1a1a1a ✓ */
}
}
/* Good: references semantic token — works in all themes */
.card {
background-color: var(--color-surface);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
/* Bad: hard-coded value that may fail in dark mode */
.card {
background-color: #ffffff;
color: #333;
}
Minor: APCA — Emerging Standard
The Advanced Perceptual Contrast Algorithm (APCA) is a candidate replacement for the WCAG 2.x contrast ratio formula expected in WCAG 3.0. It models contrast perception more accurately for thin strokes, small font sizes, and saturated colors.
APCA is not yet required. Teams adopting it today must continue to meet WCAG 2.2 AA requirements in parallel.
| Content type | Minimum Lc | Recommended Lc |
|---|---|---|
| Normal body text (16px / 400 weight) | 60 | 75 |
| Large heading text (24px+ / 700 weight) | 45 | 60 |
| UI component labels | 45 | 60 |
| Placeholder / muted text | 30 | 45 |
Recommended Contrast-Checking Tools
| Tool | Use case |
|---|---|
| WebAIM Contrast Checker | Quick manual text contrast checks |
| Colour Contrast Analyser (TPGi) | Desktop app for sampling on-screen colors |
| axe DevTools | Browser extension with contrast violation detection |
| apcacontrast.com | APCA-based contrast evaluation |
| Stark (Figma/Sketch plugin) | Design-time contrast checking |
Testing programmatically with axe-core
const axe = require("axe-core");
axe.run(document, {
runOnly: {
type: "rule",
values: ["color-contrast", "color-contrast-enhanced"]
}
}, (err, results) => {
if (err) throw err;
console.log("Contrast violations:", results.violations);
});
Definition of Done Checklist
- All normal text meets 4.5:1 contrast in light and dark modes
- All large text meets 3:1 contrast in light and dark modes
- All form controls, icons, and data visualisation elements meet 3:1 non-text contrast
- No information conveyed by color alone — icon + text + color used together
- Links in body text are distinguishable from surrounding text without color
- Focus indicators use
outline, meet 3:1 contrast, and are visible in all modes - All colour tokens defined as CSS custom properties; no hard-coded values in components
- Light mode and dark mode verified with contrast checker
- Forced-colors / Windows High Contrast Mode tested — no meaning lost
- axe-core
color-contrastrule passes in CI - Disabled controls clearly distinguished from enabled controls beyond opacity alone
Key WCAG Criteria
- 1.4.1 Use of Color (A) — Serious if failing
- 1.4.3 Contrast Minimum (AA) — Critical if failing for normal text
- 1.4.6 Contrast Enhanced (AAA)
- 1.4.11 Non-text Contrast (AA) — Serious if failing
- 2.4.7 Focus Visible (AA)
- 2.4.13 Focus Appearance (AA, WCAG 2.2) — Serious if failing
References
- Full best practices guide
- WCAG 2.2 Understanding 1.4.1 Use of Color
- WCAG 2.2 Understanding 1.4.3 Contrast (Minimum)
- WCAG 2.2 Understanding 1.4.11 Non-text Contrast
- WCAG 2.2 Understanding 2.4.13 Focus Appearance
- WCAG 2.2 Technique C40: Creating a two-color focus indicator
- APCA contrast tool
Standards horizon: WCAG 3.0's proposed APCA (Advanced Perceptual Contrast Algorithm) will replace the current luminance-ratio model with a perceptual contrast model that treats light-on-dark and dark-on-light differently. Do not apply APCA to production work until WCAG 3.0 is a published standard. Monitor: https://www.w3.org/TR/wcag-3.0/ and https://git.apcacontrast.com/