Skip to main content

Accessibility Guide

Overview

Dashtray is committed to providing an accessible experience for all users, including those using assistive technologies. This guide outlines our accessibility standards and best practices.

WCAG Compliance

We aim for WCAG 2.1 Level AA compliance across the application.

Key Principles

  1. Perceivable: Information must be presentable to users in ways they can perceive
  2. Operable: UI components must be operable by all users
  3. Understandable: Information and UI operation must be understandable
  4. Robust: Content must be robust enough to work with assistive technologies

Keyboard Navigation

Global Shortcuts

  • Tab: Navigate forward through interactive elements
  • Shift + Tab: Navigate backward through interactive elements
  • Enter/Space: Activate buttons and links
  • Escape: Close dialogs and modals
  • Arrow Keys: Navigate within lists, menus, and tabs

Skip to Content

A “Skip to main content” link appears when tabbing, allowing keyboard users to bypass navigation:
<SkipToContent />

Focus Management

  • Focus is trapped within modals and dialogs
  • Focus returns to trigger element when closing modals
  • Focus indicators are visible and meet contrast requirements
  • No positive tabindex values (disrupts natural tab order)

Screen Reader Support

ARIA Labels

All interactive elements have accessible names:
<!-- Button with icon -->
<button aria-label="Close dialog">
  <X class="h-4 w-4" />
</button>

<!-- Input with label -->
<label for="email">Email</label>
<input id="email" type="email" />

<!-- Or with aria-label -->
<input type="email" aria-label="Email address" />

ARIA Roles

Use semantic HTML first, ARIA roles when necessary:
<!-- Good: Semantic HTML -->
<nav>
  <ul>
    <li><a href="/">Home</a></li>
  </ul>
</nav>

<!-- When semantic HTML isn't enough -->
<div role="tablist">
  <button role="tab" aria-selected="true">Tab 1</button>
  <button role="tab" aria-selected="false">Tab 2</button>
</div>

Live Regions

Announce dynamic content changes:
import { announceToScreenReader } from '$lib/utils/accessibility';

// Announce success message
announceToScreenReader('Dashboard saved successfully', 'polite');

// Announce error (interrupts current announcement)
announceToScreenReader('Error: Failed to save', 'assertive');

ARIA States

Update ARIA states dynamically:
<script>
  let isExpanded = $state(false);
</script>

<button
  aria-expanded={isExpanded}
  aria-controls="content"
  onclick={() => isExpanded = !isExpanded}
>
  Toggle content
</button>

<div id="content" hidden={!isExpanded}>
  Content here
</div>

Color and Contrast

Contrast Ratios

  • Normal text: 4.5:1 minimum (WCAG AA)
  • Large text (18pt+): 3:1 minimum (WCAG AA)
  • UI components: 3:1 minimum

Testing Contrast

import { meetsContrastRequirement } from '$lib/utils/accessibility';

const isAccessible = meetsContrastRequirement(
  '#000000', // foreground
  '#FFFFFF', // background
  'AA',      // level
  'normal'   // size
);

Color Independence

Never rely on color alone to convey information:
<!-- Bad: Color only -->
<span class="text-red-500">Error</span>

<!-- Good: Color + icon + text -->
<span class="text-red-500">
  <AlertCircle class="inline h-4 w-4" />
  Error: Invalid input
</span>

Images and Media

Alternative Text

All images must have alt text:
<!-- Decorative image -->
<img src="decoration.png" alt="" />

<!-- Informative image -->
<img src="chart.png" alt="Revenue chart showing 20% growth" />

<!-- Complex image -->
<img
  src="diagram.png"
  alt="System architecture diagram"
  aria-describedby="diagram-description"
/>
<div id="diagram-description">
  Detailed description of the diagram...
</div>

Video and Audio

  • Provide captions for videos
  • Provide transcripts for audio
  • Don’t autoplay media
  • Provide controls
<video controls>
  <source src="video.mp4" type="video/mp4" />
  <track kind="captions" src="captions.vtt" srclang="en" label="English" />
</video>

Forms

Labels

Every input must have an associated label:
<!-- Explicit label -->
<label for="username">Username</label>
<input id="username" type="text" />

<!-- Implicit label -->
<label>
  Username
  <input type="text" />
</label>

<!-- aria-label when visual label isn't needed -->
<input type="search" aria-label="Search dashboards" />

Error Messages

Link error messages to inputs:
<script>
  let error = $state('');
</script>

<label for="email">Email</label>
<input
  id="email"
  type="email"
  aria-invalid={!!error}
  aria-describedby={error ? 'email-error' : undefined}
/>
{#if error}
  <div id="email-error" role="alert">
    {error}
  </div>
{/if}

Required Fields

Indicate required fields:
<label for="name">
  Name <span aria-label="required">*</span>
</label>
<input id="name" type="text" required aria-required="true" />

Interactive Components

Buttons

Buttons must have accessible text:
<!-- Text button -->
<button>Save</button>

<!-- Icon button -->
<button aria-label="Delete dashboard">
  <Trash class="h-4 w-4" />
</button>

<!-- Button with icon and text -->
<button>
  <Save class="mr-2 h-4 w-4" />
  Save Dashboard
</button>
Links must have descriptive text:
<!-- Bad: Non-descriptive -->
<a href="/docs">Click here</a>

<!-- Good: Descriptive -->
<a href="/docs">Read the documentation</a>

<!-- Icon link -->
<a href="/settings" aria-label="Settings">
  <Settings class="h-4 w-4" />
</a>

Dialogs and Modals

Dialogs must be properly labeled and manage focus:
<script>
  import { trapFocus } from '$lib/utils/accessibility';
  import { onMount } from 'svelte';

  let dialogElement: HTMLElement;

  onMount(() => {
    const cleanup = trapFocus(dialogElement);
    return cleanup;
  });
</script>

<div
  bind:this={dialogElement}
  role="dialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-description"
  aria-modal="true"
>
  <h2 id="dialog-title">Confirm Delete</h2>
  <p id="dialog-description">
    Are you sure you want to delete this dashboard?
  </p>
  <button>Cancel</button>
  <button>Delete</button>
</div>

Testing

Automated Testing

Run accessibility audit:
node scripts/accessibility-audit.js

Manual Testing

  1. Keyboard Navigation
    • Tab through entire application
    • Verify all interactive elements are reachable
    • Verify focus indicators are visible
    • Test keyboard shortcuts
  2. Screen Reader Testing
    • Test with NVDA (Windows)
    • Test with JAWS (Windows)
    • Test with VoiceOver (macOS/iOS)
    • Test with TalkBack (Android)
  3. Zoom Testing
    • Test at 200% zoom
    • Verify no horizontal scrolling
    • Verify text doesn’t overlap
  4. Color Contrast
    • Use browser DevTools contrast checker
    • Test in high contrast mode
    • Test with color blindness simulators

Browser Extensions

  • axe DevTools: Automated accessibility testing
  • WAVE: Web accessibility evaluation tool
  • Lighthouse: Includes accessibility audit
  • Color Contrast Analyzer: Check contrast ratios

Common Patterns

Loading States

<div aria-live="polite" aria-busy={isLoading}>
  {#if isLoading}
    <span class="sr-only">Loading...</span>
    <Spinner />
  {:else}
    {content}
  {/if}
</div>

Empty States

<div role="status">
  <p>No dashboards found</p>
  <button>Create your first dashboard</button>
</div>

Tooltips

<button
  aria-label="Delete dashboard"
  aria-describedby="delete-tooltip"
>
  <Trash class="h-4 w-4" />
</button>
<div id="delete-tooltip" role="tooltip">
  Delete this dashboard permanently
</div>

Data Tables

<table>
  <caption>Project metrics for last 30 days</caption>
  <thead>
    <tr>
      <th scope="col">Metric</th>
      <th scope="col">Value</th>
      <th scope="col">Change</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Revenue</th>
      <td>$10,000</td>
      <td>+20%</td>
    </tr>
  </tbody>
</table>

Checklist

  • All images have alt text
  • All inputs have labels
  • All buttons have accessible text
  • Color contrast meets WCAG AA
  • Keyboard navigation works throughout
  • Focus indicators are visible
  • Skip to content link present
  • ARIA labels on icon buttons
  • Error messages linked to inputs
  • Dialogs trap focus
  • Live regions for dynamic content
  • No positive tabindex values
  • Semantic HTML used
  • Screen reader tested
  • Lighthouse accessibility score > 90

Resources