- Consolidated multi-line arguments into single lines where appropriate in `useAuth`. - Improved spacing and readability in data processing across components (`ProfileSettingsForm`, `PasswordChangeForm`, `SessionCard`). - Applied consistent table and markdown formatting in design system docs (e.g., `README.md`, `08-ai-guidelines.md`, `00-quick-start.md`). - Updated code snippets to ensure adherence to Prettier rules and streamlined JSX structures.
17 KiB
Accessibility Guide
Build inclusive, accessible interfaces that work for everyone. Learn WCAG AA standards, keyboard navigation, screen reader support, and testing strategies.
Table of Contents
- Accessibility Standards
- Color Contrast
- Keyboard Navigation
- Screen Reader Support
- ARIA Attributes
- Focus Management
- Testing
- Accessibility Checklist
Accessibility Standards
WCAG 2.1 Level AA
We follow WCAG 2.1 Level AA as the minimum standard.
Why Level AA?
- ✅ Required for most legal compliance (ADA, Section 508)
- ✅ Covers 95%+ of accessibility needs
- ✅ Achievable without major UX compromises
- ✅ Industry standard for modern web apps
WCAG Principles (POUR):
- Perceivable - Information can be perceived by users
- Operable - Interface can be operated by users
- Understandable - Information and operation are understandable
- Robust - Content works with current and future technologies
Accessibility Decision Tree
Creating a UI element?
│
├─ Is it interactive?
│ ├─YES─> Can it be focused with Tab?
│ │ ├─YES─> ✅ Good
│ │ └─NO──> ❌ Add tabIndex or use button/link
│ │
│ └─NO──> Is it important information?
│ ├─YES─> Does it have appropriate semantic markup?
│ │ ├─YES─> ✅ Good
│ │ └─NO──> ❌ Use h1-h6, p, ul, etc.
│ │
│ └─NO──> Is it purely decorative?
│ ├─YES─> Add aria-hidden="true"
│ └─NO──> Add alt text or ARIA label
Color Contrast
Minimum Contrast Ratios (WCAG AA)
| Content Type | Minimum Ratio | Example |
|---|---|---|
| Normal text (< 18px) | 4.5:1 | Body paragraphs, form labels |
| Large text (≥ 18px or ≥ 14px bold) | 3:1 | Headings, subheadings |
| UI components | 3:1 | Buttons, form borders, icons |
| Graphical objects | 3:1 | Chart elements, infographics |
WCAG AAA (ideal, not required):
- Normal text: 7:1
- Large text: 4.5:1
Testing Color Contrast
Tools:
- WebAIM Contrast Checker
- Chrome DevTools: Inspect element → Accessibility panel
- Contrast Ratio Tool
- Browser extensions: axe DevTools, WAVE
Example:
// ✅ GOOD - 4.7:1 contrast (WCAG AA pass)
<p className="text-foreground"> // oklch(0.1529 0 0) on white
Body text
</p>
// ❌ BAD - 2.1:1 contrast (WCAG AA fail)
<p className="text-gray-400"> // Too light
Body text
</p>
// ✅ GOOD - Using semantic tokens ensures contrast
<p className="text-muted-foreground">
Secondary text
</p>
Our design system tokens are WCAG AA compliant:
text-foregroundonbg-background: 12.6:1 ✅text-primary-foregroundonbg-primary: 8.2:1 ✅text-destructiveonbg-background: 5.1:1 ✅text-muted-foregroundonbg-background: 4.6:1 ✅
Color Blindness
8% of men and 0.5% of women have some form of color blindness.
Best practices:
- ❌ Don't rely on color alone to convey information
- ✅ Use icons, text labels, or patterns in addition to color
- ✅ Test with color blindness simulators
Example:
// ❌ BAD - Color only
<div className="text-green-600">Success</div>
<div className="text-red-600">Error</div>
// ✅ GOOD - Color + icon + text
<Alert variant="success">
<CheckCircle className="h-4 w-4" />
<AlertTitle>Success</AlertTitle>
<AlertDescription>Operation completed</AlertDescription>
</Alert>
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Something went wrong</AlertDescription>
</Alert>
Keyboard Navigation
Core Requirements
All interactive elements must be:
- ✅ Focusable - Can be reached with Tab key
- ✅ Activatable - Can be triggered with Enter or Space
- ✅ Navigable - Can move between with arrow keys (where appropriate)
- ✅ Escapable - Can be closed/exited with Escape key
Tab Order
Natural tab order follows DOM order (top to bottom, left to right).
// ✅ GOOD - Natural tab order
<form>
<Input /> {/* Tab 1 */}
<Input /> {/* Tab 2 */}
<Button>Submit</Button> {/* Tab 3 */}
</form>
// ❌ BAD - Using tabIndex to force order
<form>
<Input tabIndex={2} /> // Don't do this
<Input tabIndex={1} />
<Button tabIndex={3}>Submit</Button>
</form>
When to use tabIndex:
tabIndex={0}- Make non-interactive element focusabletabIndex={-1}- Remove from tab order (for programmatic focus)tabIndex={1+}- ❌ Avoid - Breaks natural order
Keyboard Shortcuts
| Key | Action | Example |
|---|---|---|
| Tab | Move focus forward | Navigate through form fields |
| Shift + Tab | Move focus backward | Go back to previous field |
| Enter | Activate button/link | Submit form, follow link |
| Space | Activate button/checkbox | Toggle checkbox, click button |
| Escape | Close overlay | Close dialog, dropdown |
| Arrow keys | Navigate within component | Navigate dropdown items |
| Home | Jump to start | First item in list |
| End | Jump to end | Last item in list |
Implementing Keyboard Navigation
Button (automatic):
// ✅ Button is keyboard accessible by default
<Button onClick={handleClick}>Click me</Button>
// Enter or Space triggers onClick
Custom clickable div (needs work):
// ❌ BAD - Not keyboard accessible
<div onClick={handleClick}>
Click me
</div>
// ✅ GOOD - Make it accessible
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
>
Click me
</div>
// ✅ BETTER - Just use a button
<button onClick={handleClick}>
Click me
</button>
Dropdown navigation:
<DropdownMenu>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem> {/* Arrow down */}
<DropdownMenuItem>Delete</DropdownMenuItem> {/* Arrow down */}
</DropdownMenuContent>
</DropdownMenu>
// shadcn/ui handles arrow key navigation automatically
Skip Links
Allow keyboard users to skip navigation:
// Add to layout
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg"
>
Skip to main content
</a>
<nav>{/* Navigation */}</nav>
<main id="main-content">
{/* Main content */}
</main>
Screen Reader Support
Screen Reader Basics
Popular screen readers:
- NVDA (Windows) - Free, most popular for testing
- JAWS (Windows) - Industry standard, paid
- VoiceOver (macOS/iOS) - Built-in to Apple devices
- TalkBack (Android) - Built-in to Android
What screen readers announce:
- Semantic element type (button, link, heading, etc.)
- Element text content
- Element state (expanded, selected, disabled)
- ARIA labels and descriptions
Semantic HTML
Use the right HTML element for the job:
// ✅ GOOD - Semantic HTML
<header>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Page Title</h1>
<p>Content...</p>
</article>
</main>
<footer>
<p>© 2025 Company</p>
</footer>
// ❌ BAD - Div soup
<div>
<div>
<div>
<div onClick={goHome}>Home</div>
<div onClick={goAbout}>About</div>
</div>
</div>
</div>
<div>
<div>
<div>Page Title</div>
<div>Content...</div>
</div>
</div>
Semantic elements:
<header>- Page header<nav>- Navigation<main>- Main content (only one per page)<article>- Self-contained content<section>- Thematic grouping<aside>- Sidebar content<footer>- Page footer<h1>-<h6>- Headings (hierarchical)<button>- Buttons<a>- Links
Alt Text for Images
// ✅ GOOD - Descriptive alt text
<img src="/chart.png" alt="Bar chart showing 20% increase in sales from January to February" />
// ✅ GOOD - Decorative images
<img src="/decorative.png" alt="" /> // Empty alt for decorative
// OR
<img src="/decorative.png" aria-hidden="true" />
// ❌ BAD - Generic or missing alt
<img src="/chart.png" alt="image" />
<img src="/chart.png" /> // No alt
Icon-only buttons:
// ✅ GOOD - ARIA label
<Button size="icon" aria-label="Close dialog">
<X className="h-4 w-4" />
</Button>
// ❌ BAD - No label
<Button size="icon">
<X className="h-4 w-4" />
</Button>
ARIA Attributes
Common ARIA Attributes
ARIA roles:
<div role="button" tabIndex={0}>Custom Button</div>
<div role="alert">Error message</div>
<div role="status">Loading...</div>
<div role="navigation">...</div>
ARIA states:
<button aria-expanded={isOpen}>Toggle Menu</button>
<button aria-pressed={isActive}>Toggle</button>
<input aria-invalid={!!errors.email} />
<div aria-disabled="true">Disabled Item</div>
ARIA properties:
<button aria-label="Close">×</button>
<input aria-describedby="email-help" />
<input aria-required="true" />
<div aria-live="polite">Status updates</div>
<div aria-hidden="true">Decorative content</div>
Form Accessibility
Label association:
// ✅ GOOD - Explicit association
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />
// ❌ BAD - No association
<div>Email</div>
<Input type="email" />
Error messages:
// ✅ GOOD - Linked with aria-describedby
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
aria-invalid={!!errors.password}
aria-describedby={errors.password ? 'password-error' : undefined}
/>
{errors.password && (
<p id="password-error" className="text-sm text-destructive">
{errors.password.message}
</p>
)}
// ❌ BAD - No association
<Input type="password" />
{errors.password && <p>{errors.password.message}</p>}
Required fields:
// ✅ GOOD - Marked as required
<Label htmlFor="name">
Name <span className="text-destructive">*</span>
</Label>
<Input id="name" required aria-required="true" />
// Screen reader announces: "Name, required, edit text"
Live Regions
Announce dynamic updates:
// Polite (waits for user to finish)
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// Assertive (interrupts immediately)
<div aria-live="assertive" role="alert">
{errorMessage}
</div>
// Example: Toast notifications (sonner uses this)
toast.success('User created');
// Announces: "Success. User created."
Focus Management
Visible Focus Indicators
All interactive elements must have visible focus:
// ✅ GOOD - shadcn/ui components have focus rings
<Button>Click me</Button>
// Shows ring on focus
// ✅ GOOD - Custom focus styles
<div
tabIndex={0}
className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Focusable content
</div>
// ❌ BAD - Removing focus outline
<button style={{ outline: 'none' }}>Bad</button>
Use :focus-visible instead of :focus:
:focus- Shows on mouse click AND keyboard:focus-visible- Shows only on keyboard (better UX)
Focus Trapping
Dialogs should trap focus:
// shadcn/ui Dialog automatically traps focus
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
{/* Focus trapped inside */}
<Input autoFocus /> {/* Focus first field */}
<Button>Submit</Button>
</DialogContent>
</Dialog>
// When dialog closes, focus returns to trigger button
Programmatic Focus
Set focus after actions:
const inputRef = useRef<HTMLInputElement>(null);
const handleDelete = () => {
deleteUser();
// Return focus to a relevant element
inputRef.current?.focus();
};
<Input ref={inputRef} />;
Testing
Automated Testing Tools
Browser extensions:
- axe DevTools - Free, comprehensive
- WAVE - Visual feedback
- Lighthouse - Built into Chrome
CI/CD testing:
- @axe-core/react - Runtime accessibility testing
- jest-axe - Jest integration
- Playwright accessibility testing
Manual Testing Checklist
Keyboard Testing
- Unplug mouse
- Tab through entire page
- All interactive elements focusable?
- Focus indicators visible?
- Can activate with Enter/Space?
- Can close modals with Escape?
- Tab order logical?
Screen Reader Testing
- Install NVDA (Windows) or VoiceOver (Mac)
- Navigate page with screen reader on
- All content announced?
- Interactive elements have labels?
- Form errors announced?
- Heading hierarchy correct?
Contrast Testing
- Use contrast checker on all text
- Check UI components (buttons, borders)
- Test in dark mode too
- All elements meet 4.5:1 (text) or 3:1 (UI)?
Testing with Real Users
Considerations:
- Test with actual users who rely on assistive technologies
- Different screen readers behave differently
- Mobile screen readers (VoiceOver, TalkBack) differ from desktop
- Keyboard-only users have different needs than screen reader users
Accessibility Checklist
General
- Page has
<title>and<meta name="description"> - Page has proper heading hierarchy (h1 → h2 → h3)
- Landmarks used (
<header>,<nav>,<main>,<footer>) - Skip link present for keyboard users
- No content relies on color alone
Color & Contrast
- Text has 4.5:1 contrast (normal) or 3:1 (large)
- UI components have 3:1 contrast
- Tested in both light and dark modes
- Color blindness simulator used
Keyboard
- All interactive elements focusable
- Focus indicators visible (ring, outline, etc.)
- Tab order is logical
- No keyboard traps
- Enter/Space activates buttons
- Escape closes dialogs/dropdowns
- Arrow keys navigate lists/menus
Screen Readers
- All images have alt text
- Icon-only buttons have aria-label
- Form labels associated with inputs
- Form errors use aria-describedby
- Required fields marked with aria-required
- Live regions for dynamic updates
- ARIA roles used correctly
Forms
- Labels associated with inputs (
htmlFor+id) - Error messages linked (
aria-describedby) - Invalid inputs marked (
aria-invalid) - Required fields indicated (
aria-required) - Submit button disabled during submission
Focus Management
- Dialogs trap focus
- Focus returns after dialog closes
- Programmatic focus after actions
- No focus outline removed without alternative
Quick Wins for Accessibility
Easy improvements with big impact:
-
Add alt text to images
<img src="/logo.png" alt="Company Logo" /> -
Associate labels with inputs
<Label htmlFor="email">Email</Label> <Input id="email" /> -
Use semantic HTML
<button> instead of <div onClick> -
Add aria-label to icon buttons
<Button aria-label="Close"> <X /> </Button> -
Use semantic color tokens
className = 'text-foreground'; // Auto contrast -
Test with keyboard only
- Tab through page
- Fix anything unreachable
Next Steps
- Test Now: Run axe DevTools on your app
- Learn More: W3C ARIA Authoring Practices
- Components: Review accessible components
- Forms: Accessible form patterns
Related Documentation:
- Forms - Accessible form patterns
- Components - All components are accessible
- Foundations - Color contrast tokens
External Resources:
Last Updated: November 2, 2025