Skip to main content

Accessibility

Accessibility Patterns

Best practices for building accessible interfaces

Tags: pattern accessibility a11y

This guide covers accessibility best practices and patterns for building inclusive applications.

Core Principles​

  1. Perceivable - Information must be presentable in ways users can perceive
  2. Operable - Interface must be operable by all users
  3. Understandable - Information and operation must be understandable
  4. Robust - Content must be robust enough for diverse user agents

Keyboard Navigation​

Focus Management​

All interactive elements should be focusable and have visible focus indicators:

<button
  class="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
  Click me
</button>

Use AppShell's built-in skip link or add your own:

<a
  href="#main-content"
  class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-primary-foreground"
>
  Skip to main content
</a>

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

Logical Tab Order​

Ensure tab order follows visual order:

<!-- Good: natural reading order -->
<form>
  <Input label="First Name" />
  <Input label="Last Name" />
  <Input label="Email" />
  <Button type="submit">Submit</Button>
</form>

<!-- Avoid: using tabindex to reorder -->
<form>
  <Input label="Email" tabindex="3" />
  <Input label="First Name" tabindex="1" />
  <Input label="Last Name" tabindex="2" />
</form>

Focus Trapping​

Modal dialogs should trap focus:

<!-- Dialog and AlertDialog components handle this automatically -->
<Dialog open={showModal}>
  <Dialog.Content>
    <!-- Focus is trapped within -->
    <Input label="Name" />
    <Button>Save</Button>
    <Button variant="outline">Cancel</Button>
  </Dialog.Content>
</Dialog>

Screen Readers​

Semantic HTML​

Use proper HTML elements:

<!-- Good: semantic elements -->
<nav aria-label="Main navigation">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

<main>
  <article>
    <h1>Page Title</h1>
    <p>Content...</p>
  </article>
</main>

<footer>
  <p>Copyright 2024</p>
</footer>

ARIA Labels​

Provide context for screen readers:

<!-- Icon buttons need labels -->
<Button variant="ghost" size="icon" aria-label="Close dialog">
  <XIcon />
</Button>

<!-- Describe complex regions -->
<section aria-labelledby="stats-heading">
  <h2 id="stats-heading" class="sr-only">Dashboard Statistics</h2>
  <!-- Stats cards -->
</section>

<!-- Indicate current state -->
<a href="/dashboard" aria-current="page">Dashboard</a>

Live Regions​

Announce dynamic content:

<!-- Toast notifications use aria-live -->
<div role="status" aria-live="polite">
  {#each toasts as toast}
    <div>{toast.message}</div>
  {/each}
</div>

<!-- For important alerts -->
<div role="alert" aria-live="assertive">
  {#if error}
    <p>{error}</p>
  {/if}
</div>

Hidden Content​

Hide decorative or redundant content:

<!-- Decorative icons -->
<svg aria-hidden="true">...</svg>

<!-- Redundant text -->
<a href="/read-more">
  Read more
  <span class="sr-only">about our services</span>
</a>

<!-- Screen reader only text -->
<span class="sr-only">Loading, please wait</span>

Form Accessibility​

Labels​

Always associate labels with inputs:

<!-- Using FormField component -->
<FormField id="email" label="Email Address" required>
  <Input id="email" type="email" />
</FormField>

<!-- Manual association -->
<label for="password">Password</label>
<input id="password" type="password" />

Error Messages​

Link errors to inputs:

<Input id="email" aria-describedby={error ? 'email-error' : undefined} aria-invalid={!!error} />
{#if error}
  <p id="email-error" class="text-sm text-destructive">
    {error}
  </p>
{/if}

Required Fields​

Indicate required fields clearly:

<FormField label="Email" required description="We'll never share your email">
  <Input type="email" required aria-required="true" />
</FormField>

Fieldsets​

Group related fields:

<fieldset>
  <legend class="font-semibold mb-2">Contact Preferences</legend>
  <Checkbox label="Email" />
  <Checkbox label="Phone" />
  <Checkbox label="SMS" />
</fieldset>

Color and Contrast​

Color Contrast​

Ensure sufficient contrast ratios:

  • Normal text: 4.5:1 minimum
  • Large text: 3:1 minimum
  • UI components: 3:1 minimum
<!-- Theme colors are designed for contrast -->
<p class="text-foreground bg-background">High contrast text</p>
<p class="text-muted-foreground">Muted but still accessible</p>

Color Independence​

Don't rely on color alone:

<!-- Good: icon + color -->
<Alert variant="error">
  <AlertCircleIcon class="h-4 w-4" />
  Error message
</Alert>

<!-- Good: text + color -->
<Badge variant="success">Active ✓</Badge>

<!-- Avoid: color only -->
<span class="text-red-500">Required</span>

Interactive Components​

Use the right element:

<!-- Button: performs an action -->
<Button onclick={handleSave}>Save</Button>

<!-- Link: navigates somewhere -->
<a href="/settings">Settings</a>

<!-- Button styled as link (still performs action) -->
<Button variant="link" onclick={handleAction}>Do something</Button>

Disabled States​

Provide clear disabled states:

<Button disabled aria-disabled="true">Processing...</Button>

<Input disabled aria-disabled="true" />

Loading States​

Indicate loading to all users:

<Button disabled aria-busy="true">
  <Spinner class="mr-2" aria-hidden="true" />
  <span>Loading...</span>
</Button>

Motion and Animation​

Reduce Motion​

Respect user preferences:

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Theme components use Tailwind's motion-reduce: variant:

<div class="transition-all duration-300 motion-reduce:transition-none">Animated content</div>

Testing Checklist​

  • Can navigate with keyboard only
  • Focus is visible on all interactive elements
  • Screen reader announces content correctly
  • Color contrast meets WCAG AA
  • Information isn't conveyed by color alone
  • Forms have proper labels and error handling
  • Modals trap focus and can be dismissed
  • Skip links work correctly
  • Animations can be disabled
  • Touch targets are at least 44x44px

Resources​

On this page