Accessibility

View Source

Sutra UI is built with accessibility as a core principle. All components follow WAI-ARIA patterns and support keyboard navigation.

WCAG 2.1 AA Compliance

Sutra UI components are designed to meet WCAG 2.1 Level AA standards:

  • Perceivable - Text alternatives, adaptable content, distinguishable colors
  • Operable - Keyboard accessible, enough time, navigable
  • Understandable - Readable, predictable, input assistance
  • Robust - Compatible with assistive technologies

Keyboard Navigation

Global Patterns

KeyAction
TabMove focus to next focusable element
Shift + TabMove focus to previous focusable element
Enter / SpaceActivate focused element
EscapeClose modal, popover, dropdown

Component-Specific Shortcuts

Tabs

KeyAction
Arrow Left/RightNavigate between tabs
HomeGo to first tab
EndGo to last tab

Dropdown Menu

KeyAction
Arrow Up/DownNavigate menu items
EnterSelect item
EscapeClose menu

Select

KeyAction
Arrow Up/DownNavigate options
EnterSelect option
EscapeClose dropdown
TypeJump to matching option

Accordion

KeyAction
Arrow Up/DownNavigate accordion items
Enter / SpaceToggle accordion panel
HomeGo to first item
EndGo to last item

Dialog

KeyAction
EscapeClose dialog
TabCycle through focusable elements (trapped)

Command Palette

KeyAction
Arrow Up/DownNavigate results
EnterSelect item
EscapeClose palette

ARIA Attributes by Component

Buttons

<.button>Save</.button>
<!-- Renders with proper button semantics -->

<.button size="icon" aria-label="Close">
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</.button>
<!-- Icon buttons MUST have aria-label -->

<.button loading>Saving...</.button>
<!-- Sets aria-busy="true" when loading -->

Form Controls

<.field>
  <:label>Email</:label>
  <.input type="email" name="email" />
  <:error>Invalid email</:error>
</.field>
<!-- Automatically links label, input, and error with aria-describedby -->

Dialog

<.dialog id="confirm">
  <:title>Confirm Action</:title>
  <:description>Are you sure?</:description>
  Content here
</.dialog>
<!-- Sets aria-labelledby and aria-describedby automatically -->

Tabs

<.tabs id="settings" default_value="account">
  <:tab value="account">Account</:tab>
  <:panel value="account">Account settings</:panel>
</.tabs>
<!-- Full tablist/tab/tabpanel ARIA pattern -->

Focus Management

Focus Trapping

Modal components like dialog and command automatically trap focus:

  • Focus moves to the dialog when opened
  • Tab cycles through focusable elements within the dialog
  • Focus returns to the trigger element when closed

Focus Indicators

All interactive elements have visible focus indicators using the --ring CSS variable:

:root {
  --ring: oklch(0.705 0.015 286.067);  /* Focus ring color */
}

Focus indicators are:

  • Always visible (never outline: none)
  • High contrast against backgrounds
  • Consistent across all components

For page-level accessibility, add a skip link at the top of your layout:

<a href="#main-content" class="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-background focus:border focus:rounded">
  Skip to main content
</a>

<main id="main-content">
  <!-- Page content -->
</main>

Screen Reader Support

Live Regions

Toast notifications use aria-live to announce messages:

<.toaster />
<!-- Creates a live region for announcements -->

<.toast>File saved successfully</.toast>
<!-- Announced by screen readers -->

Semantic HTML

Sutra UI uses semantic HTML elements:

  • <button> for buttons (not <div>)
  • <dialog> for modals
  • <table> for data tables
  • <nav> for navigation
  • <form> for forms

Hidden Content

Use these utilities for screen reader content:

<!-- Visually hidden but accessible to screen readers -->
<span class="sr-only">Additional context</span>

<!-- Hidden from screen readers -->
<span aria-hidden="true">Decorative icon</span>

Testing Accessibility

  1. axe DevTools - Browser extension for automated testing
  2. WAVE - Web accessibility evaluation tool
  3. VoiceOver (macOS) / NVDA (Windows) - Screen reader testing
  4. Keyboard only - Navigate without a mouse

Testing Checklist

  • [ ] All interactive elements are focusable with Tab
  • [ ] Focus order is logical
  • [ ] Focus indicators are visible
  • [ ] All images have alt text
  • [ ] Form inputs have labels
  • [ ] Error messages are associated with inputs
  • [ ] Color is not the only means of conveying information
  • [ ] Text has sufficient contrast (4.5:1 for normal, 3:1 for large)

Common Accessibility Patterns

Icon Buttons

Always provide an aria-label:

<!-- Good -->
<.button size="icon" aria-label="Delete item">
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
</.button>

<!-- Bad - no accessible name -->
<.button size="icon">
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
</.button>

Loading States

Use aria-busy and announce loading:

<.button loading aria-busy="true">
  <.spinner class="mr-2" />
  Loading...
</.button>

Disabled States

Use the disabled attribute, not just styling:

<!-- Good -->
<.button disabled>Cannot submit</.button>

<!-- Bad - looks disabled but isn't -->
<.button class="opacity-50 cursor-not-allowed">Cannot submit</.button>

Form Validation

Associate errors with inputs:

<.field>
  <:label>Password</:label>
  <.input 
    type="password" 
    name="password"
    aria-invalid={@errors != []}
  />
  <:error :for={error <- @errors}>{error}</:error>
</.field>

Next Steps