- name
- wcag-contrast-color-screen-reader
- description
- Implements WCAG 2.2 AA color contrast (4.5:1 text, 3:1 UI), semantic HTML landmarks, heading hierarchy, table markup, ARIA naming, live regions, and screen reader optimization for accessible web applications.
- license
- MIT
- compatibility
- opencode
- metadata
- {"version":"1.0.0","domain":"coding","role":"implementation","scope":"implementation","output-format":"code","triggers":"color contrast, wcag accessibility, screen reader optimization, aria labels, semantic html, landmark navigation, heading hierarchy, alt text","related-skills":[],"archetypes":"tactical, diagnostic","anti_triggers":"brainstorming, vague ideation","response_profile":{"verbosity":"low","directive_strength":"high","abstraction_level":"operational"}}
# WCAG 2.2 AA Color Contrast & Screen Reader Optimization
Implement color contrast ratios (4.5:1 minimum for normal text, 3:1 for large text and UI components) and semantic HTML structure with ARIA attributes to ensure WCAG 2.2 AA compliance for assistive technology users.
## TL;DR Checklist
- [ ] Color contrast verified: 4.5:1 for body text, 3:1 for large text, 3:1 for UI components (WAVE, Axe, Lighthouse)
- [ ] Semantic HTML: one `<main>`, `<nav>`, `<header>`, `<footer>`, `<aside>` per page (landmarks)
- [ ] Heading hierarchy: `<h1>` once per page, no skipped levels (h1 → h2 → h3, never h1 → h3)
- [ ] Tables marked correctly: `<th scope="col|row">`, `<caption>`, `headers` attribute on complex tables
- [ ] Icons: `aria-label` on `<span>` icons or alt text on `<img>`; meaningful, not "icon" or "image"
- [ ] ARIA naming: follows order (aria-label > aria-labelledby > text content)
- [ ] Live regions: `aria-live="polite"` for non-urgent updates, `aria-live="assertive"` for alerts
- [ ] Status messages: `role="status"` for messages that should be announced
- [ ] Screen reader tested: VoiceOver (macOS), NVDA (Windows), or JAWS
---
## When to Use
Use this skill when:
- Building or reviewing components that must pass WCAG 2.2 AA conformance
- Designing color palettes or applying brand colors to UI
- Implementing form controls, buttons, links, or icon-based interactions
- Creating data tables, lists, or complex layouts
- Building live update regions (notifications, search results, form validation)
- Working with screen readers (VoiceOver, NVDA, JAWS) in testing
- Retrofitting existing applications to meet accessibility standards
- Ensuring color alone does not convey critical information
---
## When NOT to Use
Avoid this skill for:
- WCAG 2.1 AAA (enhanced contrast 7:1) requirements — that's a separate, stricter standard
- Automated color science or color theory deep dives — focus here is accessibility thresholds
- Screen reader automation or third-party testing integrations (use Axe, WAVE, Pa11y for those)
- PDFs or document accessibility (different standards apply — see PDF/UA)
- Video captions or audio descriptions (separate WCAG criteria: 1.2.1, 1.2.2)
- Mobile-only app accessibility (native mobile guidelines may differ)
---
## Core Workflow
### 1. Calculate & Verify Color Contrast
**Checkpoint:** Before applying any color, measure its contrast ratio against backgrounds and adjacent colors.
**Formula:** Contrast Ratio = (L1 + 0.05) / (L2 + 0.05), where L = relative luminance
- L = 0.2126 × R + 0.7152 × G + 0.0722 × B (linear RGB 0–1)
- If color channel > 0.03928: channel = channel^2.4; else: channel / 12.92
**WCAG 2.2 AA Thresholds:**
- **Normal text**: 4.5:1 (body copy, labels, any text < 18px or < 14px bold)
- **Large text**: 3:1 (text ≥ 18px or ≥ 14px bold)
- **UI components**: 3:1 (buttons, form inputs, borders, focus indicators)
- **Icons/graphics**: 3:1 (if conveying information)
**Manual Testing (when tool results are unclear):**
- Contrast over gradients: Must test the darkest/lightest pixel pair
- Contrast with images: Must test lightest + darkest image regions
- Hover/focus states: Must meet 3:1 (for non-text contrast) with adjacent color
**Tools:** [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/), [Axe DevTools](https://www.deque.com/axe/devtools/), [WAVE](https://wave.webaim.org/), [Lighthouse](https://developers.google.com/web/tools/lighthouse)
### 2. Define Color Tokens with Contrast Built-In
**Checkpoint:** Create a color palette where every color variant is paired with minimum contrast text color.
All colors in your design system should specify: "This color is ONLY for backgrounds" or "This color is ONLY for text at 4.5:1 on Color X."
**Design pattern:** Use `isAccessibleOnBackground()` function or lookup tables to prevent misuse.
### 3. Structure Semantic HTML Landmarks
**Checkpoint:** Every page must have exactly one `<main>`, and navigation must be wrapped in `<nav>`.
Landmarks allow screen reader users to jump past content:
- `<header>` — top banner (logo, title, global nav)
- `<nav>` — navigation menu (one per type: main nav, footer nav, sidebar nav)
- `<main>` — primary content (one per page)
- `<aside>` — tangential content (sidebar, related articles)
- `<footer>` — site footer (copyright, links)
### 4. Verify Heading Hierarchy
**Checkpoint:** Page starts with `<h1>`, no levels skipped (h1 → h2 → h3, never h1 → h3).
Each section must have a heading that announces its topic. Use `<h2>` to start subsections, not `<h3>`.
### 5. Mark Tables Semantically
**Checkpoint:** Header cells use `<th scope="col|row">`, data cells use `<td>`.
For complex tables: add `id` to headers and `headers` attribute on data cells pointing to multiple headers.
### 6. Label Icons & Images Meaningfully
**Checkpoint:** Every icon and image has an `aria-label` or `alt` text that describes its function, not its appearance.
Example: ❌ `aria-label="settings icon"` | ✅ `aria-label="account settings"`
### 7. Implement Live Regions for Updates
**Checkpoint:** Dynamic content (form validation results, search updates, notifications) must announce to screen readers.
Use `aria-live="polite"` for non-urgent updates, `aria-live="assertive"` for urgent alerts.
### 8. Test with Screen Readers
**Checkpoint:** Before shipping, test with at least one screen reader: VoiceOver (macOS, iOS), NVDA (Windows), or JAWS (Windows).
Read the page top-to-bottom, verify: landmarks are announced, headings make sense, form labels are read, icons are understood, live updates are heard.
---
## Implementation Patterns
### Pattern 1: CSS Color Tokens with Contrast Metadata
```typescript
// colors.ts — Design tokens with built-in accessibility guarantees
interface ColorToken {
hex: string;
name: string;
luminance: number; // L from contrast formula
textColor: "light" | "dark"; // Which text color meets 4.5:1
minContrastRatio: number; // Verified minimum ratio
}
const colors = {
neutral50: {
hex: "#F9FAFB",
name: "Neutral 50",
luminance: 0.98,
textColor: "dark", // Use dark text (e.g., #1F2937)
minContrastRatio: 12.6,
},
neutral900: {
hex: "#111827",
name: "Neutral 900",
luminance: 0.015,
textColor: "light", // Use light text (e.g., #F9FAFB)
minContrastRatio: 15.5,
},
blue500: {
hex: "#3B82F6",
name: "Blue 500",
luminance: 0.18,
textColor: "light", // White text at 4.5:1+
minContrastRatio: 5.2,
},
red500: {
hex: "#EF4444",
name: "Red 500",
luminance: 0.213,
textColor: "light", // Light text for error states
minContrastRatio: 4.7,
},
} as const;
// Helper: Ensure color is used safely
function getTextColorForBackground(bgToken: ColorToken): string {
const textColor = bgToken.textColor === "light" ? "#FFFFFF" : "#1F2937";
if (bgToken.minContrastRatio < 4.5) {
console.warn(
`⚠️ Color ${bgToken.name} has only ${bgToken.minContrastRatio.toFixed(2)}:1 contrast. Consider using a darker background.`
);
}
return textColor;
}
// Usage: Safe color application
const buttonBg = colors.blue500;
const buttonText = getTextColorForBackground(buttonBg);
// Result: buttonText = "#FFFFFF" (guaranteed 5.2:1 contrast)
```
**Why This Pattern:**
- Contrast is verified once (at token definition), then reused everywhere
- No designer can accidentally pair a 2:1 contrast color combination
- Warnings catch misuse at runtime
---
### Pattern 2: Semantic HTML Page Structure with Landmarks
```html
<!-- Complete WCAG 2.2 AA page structure -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Product Dashboard — MyApp</title>
</head>
<body>
<!-- LANDMARK 1: Header with branding -->
<header role="banner" aria-label="Site header">
<div class="header-content">
<a href="/" class="logo" aria-label="MyApp home">
<img src="logo.svg" alt="MyApp" />
</a>
<!-- LANDMARK 2: Primary navigation -->
<nav aria-label="Main navigation">
<ul>
<li>
<a href="/dashboard" aria-current="page">Dashboard</a>
</li>
<li><a href="/analytics">Analytics</a></li>
<li><a href="/settings">Settings</a></li>
</ul>
</nav>
</div>
</header>
<div class="page-layout">
<!-- LANDMARK 3: Sidebar navigation (secondary) -->
<aside aria-label="Sidebar navigation">
<nav aria-label="Filters">
<fieldset>
<legend>Filter by status</legend>
<label>
<input type="checkbox" name="status" value="active" />
Active
</label>
<label>
<input type="checkbox" name="status" value="pending" />
Pending
</label>
</fieldset>
</nav>
</aside>
<!-- LANDMARK 4: Main content area (only one per page) -->
<main id="main-content" aria-label="Dashboard content">
<!-- Section with heading hierarchy -->
<h1>Sales Dashboard</h1>
<!-- Key metrics section -->
<section aria-labelledby="metrics-heading">
<h2 id="metrics-heading">Q4 Metrics</h2>
<!-- Grid of metric cards with proper ARIA -->
<div class="metrics-grid">
<article class="metric-card">
<h3>Total Revenue</h3>
<p class="metric-value" aria-label="Total revenue: 124 thousand dollars">
$124K
</p>
<p class="metric-change" aria-label="increase of 12 percent">
↑ 12%
</p>
</article>
<article class="metric-card">
<h3>Active Users</h3>
<p class="metric-value" aria-label="Active users: 3 thousand 420">
3.4K
</p>
<p class="metric-change" aria-label="increase of 8 percent">
↑ 8%
</p>
</article>
</div>
</section>
<!-- Data table with accessibility markup -->
<section aria-labelledby="table-heading">
<h2 id="table-heading">Recent Orders</h2>
<table>
<caption>Orders from the past 7 days, sorted by date</caption>
<thead>
<tr>
<th scope="col">Order ID</th>
<th scope="col">Customer</th>
<th scope="col">Total</th>
<th scope="col">Status</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>ORD-001</td>
<td>Alice Smith</td>
<td>$2,500</td>
<td>
<span
class="badge badge-completed"
role="status"
aria-label="Completed"
>
Completed
</span>
</td>
<td>
<button aria-label="View order ORD-001 details">View</button>
</td>
</tr>
<tr>
<td>ORD-002</td>
<td>Bob Johnson</td>
<td>$1,200</td>
<td>
<span
class="badge badge-pending"
role="status"
aria-label="Pending"
>
Pending
</span>
</td>
<td>
<button aria-label="View order ORD-002 details">View</button>
</td>
</tr>
</tbody>
</table>
</section>
<!-- Live region for dynamic updates -->
<div
id="search-results"
aria-live="polite"
aria-label="Search results"
role="status"
>
<!-- JavaScript updates this area; screen readers announce changes -->
</div>
</main>
</div>
<!-- LANDMARK 5: Footer -->
<footer role="contentinfo" aria-label="Site footer">
<nav aria-label="Footer navigation">
<ul>
<li><a href="/privacy">Privacy Policy</a></li>
<li><a href="/terms">Terms of Service</a></li>
<li><a href="/accessibility">Accessibility</a></li>
</ul>
</nav>
<p>© 2026 MyApp, Inc. All rights reserved.</p>
</footer>
</body>
Auf GitHub ansehen