Accessibility
Last updated on 2026-05-31
The SaaS Metrics Kit is built with WCAG AA accessibility as a baseline. Every component and page meets or exceeds these standards, using semantic HTML, ARIA attributes, and accessible interaction patterns throughout all 25 screens.
Skip Link
Every page includes a skip-to-content link as the first focusable element:
import { SkipLink } from "@/components/layout/skip-link"
<SkipLink />
The skip link is visually hidden until focused. It jumps the user directly to the #main-content landmark, bypassing the sidebar and header navigation.
Color Contrast
All text meets WCAG AA contrast ratios:
- Normal text (< 18px): minimum 4.5:1 contrast ratio
- Large text (>= 18px bold or >= 24px): minimum 3:1 contrast ratio
- UI components and states: minimum 3:1 against adjacent colors
The oklch color token system ensures contrast is maintained in both light and dark mode. The blue-violet theme (hue 260) was specifically tuned so that primary-on-background and foreground-on-muted combinations pass AA thresholds.
Keyboard Navigation
All interactive elements are keyboard accessible:
- Tab -- navigate between focusable elements in logical order
- Shift+Tab -- navigate backwards
- Enter/Space -- activate buttons, links, and toggles
- Arrow keys -- navigate within menus, radio groups, tabs, and select dropdowns
- Escape -- close dialogs, sheets, popovers, and dropdown menus
Focus Indicators
Visible focus rings appear on all interactive elements using the --ring design token:
:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
Focus Traps
Focus is trapped inside overlay components to prevent keyboard users from tabbing behind them:
- Dialog -- invite member dialog on the team settings page, confirmation dialogs
- Sheet -- mobile sidebar navigation
- DropdownMenu -- bulk actions on invoices, user menu
When these components close, focus returns to the trigger element that opened them.
Screen Reader Support
Semantic HTML
All pages use proper HTML5 landmarks:
<header>for the app header<nav>witharia-labelfor sidebar navigation and tab navigation<main>for primary content area<aside>for the sidebar<section>witharia-labelfor dashboard widgets, chart sections, and settings groups<time>withdatetimeattribute for timestamps throughout activity feeds and tables
ARIA Labels
Icon-only buttons include descriptive labels:
<Button aria-label="Search customers">
<Search className="size-4" />
</Button>
<Button aria-label="Switch to dark mode">
<Moon className="size-5" />
</Button>
Date inputs include aria-label for screen reader context:
<Input type="date" aria-label="From date" />
<Input type="date" aria-label="To date" />
Live Regions
The LiveRegion component announces dynamic content changes to screen readers:
import { LiveRegion } from "@/components/a11y/live-region"
<LiveRegion message="Settings saved successfully" politeness="polite" />
Live regions are used alongside sonner toast notifications for subscription status changes, alert rule toggles, filter updates, and form submissions.
Visually Hidden Text
The VisuallyHidden component provides text for screen readers without affecting visual layout:
import { VisuallyHidden } from "@/components/a11y/visually-hidden"
<VisuallyHidden>Revenue increased by 12% this month</VisuallyHidden>
Heading Hierarchy
Every page follows a strict heading hierarchy:
- One
<h1>per page (the page title, rendered byPageHeader) <h2>for major sections (card titles, section headings)<h3>for subsections within those sections- No skipped heading levels
For example, the dashboard uses <h1> for "Dashboard", and <h2> for "MRR Trend", "Customer Growth", "MRR Movement", "Recent Activity", and "Quick Actions" sections.
Touch Targets
All interactive elements meet the 44x44px minimum touch target size. The kit includes a .touch-target utility class:
.touch-target {
min-width: 44px;
min-height: 44px;
}
This class is applied to icon buttons in the app header (theme toggle, user menu), sidebar navigation items, table row actions (download, delete, revoke), and pagination controls on mobile.
Reduced Motion
The kit respects the prefers-reduced-motion media query. When enabled:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
This disables all animations including chart entrance animations, card fade-in-up transitions, sidebar collapse/expand transitions, shimmer loading states, floating decorative elements, stagger delay sequences, and button press micro-interactions.
The use-reduced-motion hook is also available for programmatic checks:
import { useReducedMotion } from "@/hooks/use-reduced-motion"
const prefersReducedMotion = useReducedMotion()
Status Indicators
Status information is never conveyed by color alone. The kit uses color plus icon plus text:
{/* Trend uses icon + text + color */}
<TrendingUp className="size-4 text-green-600" />
<span className="text-green-600">+5.2%</span>
{/* Alert severity uses badge + text + color */}
<StatusBadge status="critical" variant="error" />
The StatusBadge component combines color-coded badges with readable text labels (active, trialing, past_due, canceled, connected, disconnected, critical, warning, info) so no information depends solely on color.
Form Accessibility
Forms across the kit follow accessibility best practices:
- Labels associated with inputs via
htmlFor/idpairing - Error messages linked with
aria-describedby - Required fields marked with visual indicators and
aria-required - Invalid fields marked with
aria-invalidfor screen reader announcement - The settings forms, profile form, team invite dialog, and filter controls all follow these patterns
- Toast notifications from
sonnerannounce form submission results
Table Accessibility
Data tables throughout the kit use semantic <table>, <thead>, <tbody>, <th>, and <td> elements:
- Column headers use
<TableHead>with proper scope - Clickable rows include
cursor-pointerstyling for visual affordance - Responsive columns use
hidden md:table-cellto progressively show content on larger screens while maintaining table semantics - Monospace formatting on numeric values uses
tabular-numsfor consistent alignment
Accessibility Hooks
The kit includes five accessibility-focused hooks:
| Hook | Purpose |
|---|---|
use-announce |
Programmatically announce messages to screen readers via ARIA live regions |
use-focus-trap |
Trap focus within modal dialogs and overlay components |
use-keyboard-navigation |
Enable keyboard navigation patterns (arrow keys, escape) |
use-mobile |
Detect mobile viewport for responsive accessibility adjustments |
use-reduced-motion |
Detect prefers-reduced-motion preference |
Testing Recommendations
| Tool | Purpose |
|---|---|
| axe DevTools | Automated accessibility scanning |
| VoiceOver (macOS) | Screen reader testing |
| NVDA (Windows) | Screen reader testing |
| Keyboard-only navigation | Tab through every page |
| Lighthouse | Accessibility audit score |
Need a professional accessibility review? Try our free WCAG compliance checker to scan any URL for accessibility issues.