13. Accessibility (WCAG 2.1 AA)
Overview
Dashboard 2.0 meets WCAG 2.1 Level AA accessibility standards to ensure usability for all users, including those with visual, motor, cognitive, and hearing disabilities. Accessibility is not an afterthought—it's integrated into every component from the ground up.
Compliance Target: WCAG 2.1 AA Testing Tools: axe DevTools, NVDA, VoiceOver, JAWS Validation: Automated + manual testing across all major screen readers
13.1 Keyboard Navigation
Full Keyboard Access
All functionality must be accessible via keyboard alone (WCAG 2.1.1).
Standard Keyboard Shortcuts:
- Tab: Move focus to next interactive element
- Shift+Tab: Move focus to previous element
- Enter/Space: Activate buttons and links
- Arrow keys: Navigate within components (dropdowns, Kanban boards)
- Esc: Close modals and dropdowns
- Cmd/Ctrl+K: Open global search
Tab Order
Logical tab order following visual flow (top→bottom, left→right):
<!-- Example: Proper tab order for Kanban board -->
<div class="kanban-board">
<button class="add-column" tabindex="0">Add Column</button>
<div class="kanban-column" tabindex="0">
<h3>To Do</h3>
<div class="task-card" tabindex="0">Task 1</div>
<div class="task-card" tabindex="0">Task 2</div>
</div>
<div class="kanban-column" tabindex="0">
<h3>In Progress</h3>
<div class="task-card" tabindex="0">Task 3</div>
</div>
</div>
Focus Indicators
Visible focus indicators for all interactive elements (WCAG 2.4.7):
/* Global focus style */
*:focus {
outline: 3px solid #0066CC;
outline-offset: 2px;
}
/* Custom focus for buttons */
button:focus {
outline: 3px solid #0066CC;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.2);
}
/* Skip focus for mouse clicks (only keyboard) */
button:focus:not(:focus-visible) {
outline: none;
box-shadow: none;
}
button:focus-visible {
outline: 3px solid #0066CC;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.2);
}
Skip Links
Allow keyboard users to skip navigation (WCAG 2.4.1):
<a href="#main-content" class="skip-link">Skip to main content</a>
<a href="#navigation" class="skip-link">Skip to navigation</a>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px 16px;
background: #0066CC;
color: #FFFFFF;
z-index: 10000;
}
.skip-link:focus {
top: 0;
}
</style>
13.2 Screen Reader Support
ARIA Labels and Roles
Proper ARIA attributes for all interactive components (WCAG 4.1.2):
Navigation
<nav aria-label="Main navigation">
<ul role="list">
<li role="listitem">
<a href="/dashboard" aria-current="page">Dashboard</a>
</li>
<li role="listitem">
<a href="/projects">Projects</a>
</li>
</ul>
</nav>
Buttons
<button aria-label="Close modal" aria-pressed="false">
<span aria-hidden="true">✕</span>
</button>
<button aria-label="Filter tasks" aria-expanded="false" aria-controls="filter-modal">
Filter
</button>
Search
<div role="search">
<input
type="text"
aria-label="Search tasks, projects, checkpoints"
aria-autocomplete="list"
aria-controls="search-results"
aria-expanded="false"
/>
<div id="search-results" role="listbox" aria-label="Search results">
<div role="option" aria-selected="false">Result 1</div>
</div>
</div>
Kanban Board
<div class="kanban-board" role="region" aria-label="Task board">
<div class="kanban-column" role="list" aria-label="To Do tasks">
<div
class="task-card"
role="listitem"
tabindex="0"
aria-label="Implement user authentication, Priority: P0, Status: Pending"
>
<!-- Task content -->
</div>
</div>
</div>
Modals
<div
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<h2 id="modal-title">Task Details</h2>
<p id="modal-description">View and edit task information</p>
<!-- Modal content -->
</div>
Live Regions
Announce dynamic updates to screen readers (WCAG 4.1.3):
<!-- Status messages -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
<span id="status-message">Task moved to In Progress</span>
</div>
<!-- Error messages -->
<div aria-live="assertive" aria-atomic="true" class="sr-only">
<span id="error-message">Failed to save task</span>
</div>
// Announce status changes
function announceStatus(message) {
const statusEl = document.getElementById('status-message');
statusEl.textContent = message;
}
// Announce errors
function announceError(message) {
const errorEl = document.getElementById('error-message');
errorEl.textContent = message;
}
Screen Reader Only Text
Provide context for screen reader users:
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
</style>
<!-- Example: Icon-only button -->
<button aria-label="Delete task">
<span class="sr-only">Delete task</span>
<span aria-hidden="true">🗑️</span>
</button>
13.3 Color Contrast
WCAG AA Contrast Ratios
Minimum contrast ratios (WCAG 1.4.3):
- Normal text: 4.5:1 minimum
- Large text (18px+ or 14px+ bold): 3:1 minimum
- UI components and graphical objects: 3:1 minimum
Color Palette with Verified Ratios
/* Primary Colors */
--color-primary: #0066CC; /* Blue */
--color-text-dark: #111827; /* Nearly black */
--color-text-medium: #6B7280; /* Medium gray */
--color-text-light: #9CA3AF; /* Light gray */
--color-background: #FFFFFF; /* White */
/* Contrast Ratios */
/* #111827 on #FFFFFF = 16.1:1 ✅ (Exceeds AAA) */
/* #6B7280 on #FFFFFF = 4.6:1 ✅ (Passes AA) */
/* #9CA3AF on #FFFFFF = 2.8:1 ❌ (Fails AA - use only for decorative) */
/* #0066CC on #FFFFFF = 7.5:1 ✅ (Exceeds AAA) */
/* Status Colors */
--status-success: #10B981; /* Green - 3.4:1 on white ✅ */
--status-warning: #F59E0B; /* Orange - 2.9:1 on white ❌ */
--status-warning-dark: #D97706; /* Dark orange - 4.5:1 on white ✅ */
--status-error: #EF4444; /* Red - 4.0:1 on white ✅ */
--status-info: #3B82F6; /* Blue - 5.9:1 on white ✅ */
/* Backgrounds */
--bg-gray-50: #F9FAFB;
--bg-gray-100: #F3F4F6;
--bg-gray-200: #E5E7EB;
Contrast Testing
Manual testing with tools:
- Chrome DevTools: Lighthouse audit
- axe DevTools: Automated contrast check
- WebAIM Contrast Checker: https://webaim.org/resources/contrastchecker/
// Automated contrast check example
function checkContrast(foreground, background) {
const contrast = calculateContrastRatio(foreground, background);
if (contrast >= 4.5) {
console.log('✅ Passes AA for normal text');
} else if (contrast >= 3.0) {
console.log('⚠️ Passes AA for large text only');
} else {
console.error('❌ Fails AA - contrast too low');
}
}
Non-Color Indicators
Never rely on color alone (WCAG 1.4.1):
<!-- Bad: Color-only status -->
<div style="background: red;">Error</div>
<!-- Good: Color + icon + text -->
<div class="status-error">
<span class="icon" aria-hidden="true">⚠️</span>
<span>Error</span>
</div>
<!-- Good: Color + pattern (for charts) -->
<svg>
<rect fill="#10B981" stroke-dasharray="5,5" /> <!-- Striped green -->
<rect fill="#EF4444" stroke-dasharray="10,2" /> <!-- Dotted red -->
</svg>
13.4 Text Alternatives
Images and Icons
Provide alt text for all images (WCAG 1.1.1):
<!-- Informative image -->
<img src="dashboard-chart.png" alt="Task completion chart showing 78% progress" />
<!-- Decorative image -->
<img src="decorative-line.png" alt="" role="presentation" />
<!-- Icon with text -->
<button>
<svg aria-hidden="true"><path d="..." /></svg>
Save
</button>
<!-- Icon-only button -->
<button aria-label="Save task">
<svg aria-hidden="true"><path d="..." /></svg>
</button>
Complex Graphics
Provide detailed descriptions for charts and diagrams:
<figure role="group" aria-labelledby="chart-title" aria-describedby="chart-description">
<h3 id="chart-title">Task Completion Rate</h3>
<p id="chart-description" class="sr-only">
Bar chart showing task completion rate over 8 weeks.
Week 1: 45%, Week 2: 52%, Week 3: 61%, Week 4: 68%,
Week 5: 72%, Week 6: 75%, Week 7: 77%, Week 8: 78%.
Overall trend is steadily increasing.
</p>
<canvas id="completion-chart"></canvas>
<!-- Optional: Data table alternative -->
<details>
<summary>View data table</summary>
<table>
<tr><th>Week</th><th>Completion Rate</th></tr>
<tr><td>Week 1</td><td>45%</td></tr>
<tr><td>Week 2</td><td>52%</td></tr>
<!-- ... -->
</table>
</details>
</figure>
13.5 Forms and Inputs
Labels and Instructions
All inputs must have labels (WCAG 1.3.1, 3.3.2):
<!-- Visible label -->
<label for="task-title">Task Title</label>
<input id="task-title" type="text" required aria-required="true" />
<!-- Hidden label (use sparingly) -->
<input type="text" aria-label="Search tasks" />
<!-- Group labels -->
<fieldset>
<legend>Task Priority</legend>
<input type="radio" id="p0" name="priority" value="P0" />
<label for="p0">P0 - Critical</label>
<input type="radio" id="p1" name="priority" value="P1" />
<label for="p1">P1 - High</label>
</fieldset>
Error Identification
Clearly identify errors (WCAG 3.3.1):
<label for="email">Email Address</label>
<input
id="email"
type="email"
aria-invalid="true"
aria-describedby="email-error"
required
/>
<div id="email-error" class="error-message" role="alert">
Please enter a valid email address
</div>
<style>
.error-message {
color: #DC2626; /* Red with 4.5:1 contrast */
font-weight: 600;
margin-top: 4px;
}
input[aria-invalid="true"] {
border-color: #DC2626;
border-width: 2px;
}
</style>
Error Suggestions
Provide helpful error suggestions (WCAG 3.3.3):
<div id="password-error" role="alert">
<strong>Password is too weak.</strong>
<ul>
<li>Use at least 12 characters</li>
<li>Include uppercase and lowercase letters</li>
<li>Include at least one number</li>
</ul>
</div>
13.6 Timing and Animations
No Time Limits
Avoid time limits or make them adjustable (WCAG 2.2.1):
// Bad: Fixed timeout
setTimeout(closeModal, 5000);
// Good: User can extend or disable
let timeout;
function startTimeout() {
timeout = setTimeout(() => {
showTimeoutWarning();
}, 30000); // 30 seconds before warning
}
function showTimeoutWarning() {
const confirmed = confirm('Session about to expire. Extend?');
if (confirmed) {
startTimeout(); // Restart
}
}
Pause Animations
Allow users to pause animations (WCAG 2.2.2, 2.3.3):
<button id="pause-animations" aria-label="Pause animations">
<span id="pause-icon">⏸️</span>
<span id="pause-text">Pause Animations</span>
</button>
<style>
/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
Reduced Motion
Respect user's motion preferences:
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
// Disable animations
document.body.classList.add('reduce-motion');
}
13.7 Responsive Text
Resize Text (WCAG 1.4.4)
Allow text resize up to 200% without loss of functionality:
/* Use relative units (rem, em) instead of px */
body {
font-size: 16px; /* Base size */
}
h1 {
font-size: 2rem; /* 32px, scales with user preferences */
}
p {
font-size: 1rem; /* 16px, scales with user preferences */
}
/* Avoid fixed widths that break on zoom */
.container {
max-width: 90rem; /* Flexible width */
padding: 2rem;
}
Reflow (WCAG 1.4.10)
Content reflows without horizontal scrolling at 320px width:
/* Responsive layout */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
/* Avoid fixed widths */
.card {
max-width: 100%; /* Never exceed container */
padding: 1rem;
}
13.8 Testing Checklist
Automated Testing
Run automated tools (catch 30-40% of issues):
# axe DevTools (Chrome extension)
# Lighthouse (Chrome DevTools)
# WAVE (Browser extension)
# Command-line testing
npm install -g @axe-core/cli
axe http://localhost:8000 --rules wcag2a,wcag2aa
Manual Testing
Screen readers:
- NVDA (Windows) - Free, most popular
- JAWS (Windows) - Widely used in enterprise
- VoiceOver (Mac/iOS) - Apple devices
- TalkBack (Android) - Android devices
Keyboard testing:
- Navigate entire dashboard with Tab/Shift+Tab
- All interactive elements reachable
- Focus indicators visible
- Esc closes modals/dropdowns
- Arrow keys work in components
Visual testing:
- Zoom to 200% (Cmd/Ctrl +)
- Test grayscale mode (color blindness simulation)
- Test high contrast mode (Windows)
- Check all text contrast ratios
Responsive testing:
- Test on 320px width (smallest mobile)
- Ensure no horizontal scroll
- Touch targets ≥44px
13.9 WCAG 2.1 AA Compliance Checklist
Perceivable
- 1.1.1 Non-text content has text alternatives
- 1.3.1 Info and relationships conveyed programmatically
- 1.4.1 Color not used as only means of conveying information
- 1.4.3 Contrast ratio ≥4.5:1 for normal text, ≥3:1 for large text
- 1.4.4 Text resizable to 200% without loss of content
- 1.4.10 Content reflows without horizontal scrolling at 320px
- 1.4.11 Non-text contrast ≥3:1 for UI components
Operable
- 2.1.1 All functionality available from keyboard
- 2.1.2 No keyboard trap
- 2.4.1 Skip links provided
- 2.4.3 Focus order is logical
- 2.4.6 Headings and labels are descriptive
- 2.4.7 Focus indicator visible
- 2.5.5 Target size ≥44x44px (mobile)
Understandable
- 3.1.1 Language of page identified (lang="en")
- 3.2.1 On focus doesn't trigger unexpected changes
- 3.2.2 On input doesn't trigger unexpected changes
- 3.3.1 Errors identified in text
- 3.3.2 Labels or instructions provided for inputs
- 3.3.3 Error suggestions provided
Robust
- 4.1.1 Valid HTML (no parsing errors)
- 4.1.2 Name, role, value for all UI components
- 4.1.3 Status messages programmatically determined
13.10 Implementation Prompt - Accessibility
Create a fully accessible dashboard meeting WCAG 2.1 AA standards.
HTML Structure
- Semantic HTML: Use
<nav>,<main>,<section>,<article>,<aside> - Proper headings: H1 → H2 → H3 hierarchy
- ARIA labels: For all interactive elements
- Skip links: Jump to main content
- Focus management: Trap focus in modals
CSS Requirements
- Focus indicators: 3px outline, 2px offset
- Contrast ratios: Verify all text ≥4.5:1
- Responsive text: Use rem/em units
- Reduced motion: Respect prefers-reduced-motion
JavaScript Requirements
- Keyboard handlers: Arrow keys, Esc, Enter
- Live regions: Announce dynamic updates
- Focus management: Move focus to modals when opened
- Error handling: Clear error messages with suggestions
Testing
- Automated: axe DevTools, Lighthouse (95+ score)
- Manual: NVDA, JAWS, VoiceOver
- Keyboard: 100% navigable without mouse
- Zoom: 200% zoom without horizontal scroll
Next: 14. Technical Implementation Previous: 12. Responsive Design Index: Master Index