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

17 KiB
Raw Permalink Blame History

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
  2. Color Contrast
  3. Keyboard Navigation
  4. Screen Reader Support
  5. ARIA Attributes
  6. Focus Management
  7. Testing
  8. 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:

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-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:

// ❌ 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).

// ✅ 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):

// ✅ 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

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>&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

// ✅ 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:

CI/CD 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

    <img src="/logo.png" alt="Company Logo" />
    
  2. Associate labels with inputs

    <Label htmlFor="email">Email</Label>
    <Input id="email" />
    
  3. Use semantic HTML

    <button> instead of <div onClick>
    
  4. Add aria-label to icon buttons

    <Button aria-label="Close">
      <X />
    </Button>
    
  5. Use semantic color tokens

    className = 'text-foreground'; // Auto contrast
    
  6. Test with keyboard only

    • Tab through page
    • Fix anything unreachable

Next Steps


Related Documentation:

External Resources:

Last Updated: November 2, 2025