Files
Felipe Cardoso 96df7edf88 Refactor useAuth hook, settings components, and docs for formatting and readability improvements
- 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.
2025-11-10 11:03:45 +01:00

746 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
1. [Accessibility Standards](#accessibility-standards)
2. [Color Contrast](#color-contrast)
3. [Keyboard Navigation](#keyboard-navigation)
4. [Screen Reader Support](#screen-reader-support)
5. [ARIA Attributes](#aria-attributes)
6. [Focus Management](#focus-management)
7. [Testing](#testing)
8. [Accessibility Checklist](#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):**
1. **Perceivable** - Information can be perceived by users
2. **Operable** - Interface can be operated by users
3. **Understandable** - Information and operation are understandable
4. **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](https://webaim.org/resources/contrastchecker/)
- Chrome DevTools: Inspect element → Accessibility panel
- [Contrast Ratio Tool](https://contrast-ratio.com)
- Browser extensions: axe DevTools, WAVE
**Example:**
```tsx
// ✅ 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-foreground` on `bg-background`: 12.6:1 ✅
- `text-primary-foreground` on `bg-primary`: 8.2:1 ✅
- `text-destructive` on `bg-background`: 5.1:1 ✅
- `text-muted-foreground` on `bg-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:**
```tsx
// ❌ 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:
1.**Focusable** - Can be reached with Tab key
2.**Activatable** - Can be triggered with Enter or Space
3.**Navigable** - Can move between with arrow keys (where appropriate)
4.**Escapable** - Can be closed/exited with Escape key
---
### Tab Order
**Natural tab order** follows DOM order (top to bottom, left to right).
```tsx
// ✅ 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 focusable
- `tabIndex={-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):**
```tsx
// ✅ Button is keyboard accessible by default
<Button onClick={handleClick}>Click me</Button>
// Enter or Space triggers onClick
```
**Custom clickable div (needs work):**
```tsx
// ❌ 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:**
```tsx
<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:**
```tsx
// 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:**
```tsx
// ✅ 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>&copy; 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
```tsx
// ✅ 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:**
```tsx
// ✅ 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:**
```tsx
<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:**
```tsx
<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:**
```tsx
<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:**
```tsx
// ✅ GOOD - Explicit association
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />
// ❌ BAD - No association
<div>Email</div>
<Input type="email" />
```
**Error messages:**
```tsx
// ✅ 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:**
```tsx
// ✅ 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:**
```tsx
// 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:**
```tsx
// ✅ 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:**
```tsx
// 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:**
```tsx
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](https://www.deque.com/axe/devtools/) - Free, comprehensive
- [WAVE](https://wave.webaim.org/extension/) - Visual feedback
- [Lighthouse](https://developer.chrome.com/docs/lighthouse/) - Built into Chrome
**CI/CD testing:**
- [@axe-core/react](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/react) - Runtime accessibility testing
- [jest-axe](https://github.com/nickcolley/jest-axe) - Jest integration
- [Playwright accessibility testing](https://playwright.dev/docs/accessibility-testing)
---
### Manual Testing Checklist
#### Keyboard Testing
1. [ ] Unplug mouse
2. [ ] Tab through entire page
3. [ ] All interactive elements focusable?
4. [ ] Focus indicators visible?
5. [ ] Can activate with Enter/Space?
6. [ ] Can close modals with Escape?
7. [ ] Tab order logical?
#### Screen Reader Testing
1. [ ] Install NVDA (Windows) or VoiceOver (Mac)
2. [ ] Navigate page with screen reader on
3. [ ] All content announced?
4. [ ] Interactive elements have labels?
5. [ ] Form errors announced?
6. [ ] Heading hierarchy correct?
#### Contrast Testing
1. [ ] Use contrast checker on all text
2. [ ] Check UI components (buttons, borders)
3. [ ] Test in dark mode too
4. [ ] 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:**
1. **Add alt text to images**
```tsx
<img src="/logo.png" alt="Company Logo" />
```
2. **Associate labels with inputs**
```tsx
<Label htmlFor="email">Email</Label>
<Input id="email" />
```
3. **Use semantic HTML**
```tsx
<button> instead of <div onClick>
```
4. **Add aria-label to icon buttons**
```tsx
<Button aria-label="Close">
<X />
</Button>
```
5. **Use semantic color tokens**
```tsx
className = 'text-foreground'; // Auto contrast
```
6. **Test with keyboard only**
- Tab through page
- Fix anything unreachable
---
## Next Steps
- **Test Now**: Run [axe DevTools](https://www.deque.com/axe/devtools/) on your app
- **Learn More**: [W3C ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
- **Components**: [Review accessible components](./02-components.md)
- **Forms**: [Accessible form patterns](./06-forms.md)
---
**Related Documentation:**
- [Forms](./06-forms.md) - Accessible form patterns
- [Components](./02-components.md) - All components are accessible
- [Foundations](./01-foundations.md) - Color contrast tokens
**External Resources:**
- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [A11y Project Checklist](https://www.a11yproject.com/checklist/)
- [axe DevTools](https://www.deque.com/axe/devtools/)
**Last Updated**: November 2, 2025