Skip to main content

Overview

Stride Design System is built on React Aria Components by Adobe, ensuring all components meet WCAG 2.1 Level AA standards out of the box. No additional accessibility work required. What you get automatically:
  • Full keyboard navigation
  • Screen reader support (ARIA labels, roles, states)
  • Focus management
  • Touch-friendly targets (44px minimum)
  • Color contrast compliance
  • SSR compatibility

React Aria Components Foundation

Every interactive component extends React Aria primitives:
import { Button as AriaButton } from 'react-aria-components';

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ children, ...props }, ref) => {
    return (
      <AriaButton ref={ref} {...props}>
        {children}
      </AriaButton>
    );
  }
);

// Automatic features:
// ✅ Keyboard: Enter/Space to activate
// ✅ Focus: Visible focus ring
// ✅ Disabled: aria-disabled attribute
// ✅ Screen reader: Proper role="button"

Keyboard Navigation

All components support standard keyboard patterns:
KeyAction
SpaceToggle checkbox / Select radio
TabMove focus to next control
Arrow keysNavigate radio group
<CheckboxGroup>
  <Checkbox value="a">Option A</Checkbox>
  <Checkbox value="b">Option B</Checkbox>
</CheckboxGroup>
// Arrow keys navigate, Space toggles
KeyAction
Enter / SpaceOpen dropdown
Arrow Up/DownNavigate options
EnterSelect option
EscapeClose dropdown
Home / EndJump to first/last option
TypeFilter options (typeahead)
<Select>
  <SelectItem>Option 1</SelectItem>
  <SelectItem>Option 2</SelectItem>
</Select>
// Full keyboard navigation + typeahead search
KeyAction
EscapeClose dialog
TabCycle through focusable elements
<Dialog>
  <DialogTitle>Confirm</DialogTitle>
  <DialogDescription>Are you sure?</DialogDescription>
  <Button>Yes</Button>
  <Button>Cancel</Button>
</Dialog>
// Focus trapped inside, ESC to close
KeyAction
Arrow Left/RightNavigate tabs
Home / EndJump to first/last tab
TabMove focus to tab panel
<Tabs>
  <TabList>
    <Tab>Tab 1</Tab>
    <Tab>Tab 2</Tab>
  </TabList>
  <TabPanel>Content 1</TabPanel>
  <TabPanel>Content 2</TabPanel>
</Tabs>
// Arrow keys navigate, Enter activates

Screen Reader Support

Components automatically announce their state and changes:
<Button isDisabled>
  Submit
</Button>
// Screen reader: "Submit, button, dimmed"

<Button aria-label="Close dialog">
  <XIcon />
</Button>
// Screen reader: "Close dialog, button"

Focus Management

Stride uses CSS custom properties for consistent focus indicators:
Focus Ring System
:root {
  --focus-ring-color: var(--border-focus);
  --focus-ring-width: 2px;
  --focus-ring-offset: 1px;
}

.focus-ring {
  @apply focus:outline-none focus:ring-2 focus:ring-offset-1;
  --tw-ring-color: var(--border-focus);
}

.focus-ring-danger {
  @apply focus:outline-none focus:ring-2 focus:ring-offset-1;
  --tw-ring-color: var(--status-danger);
}
All interactive elements have visible focus indicators:
<Button>Focus me</Button>
  • Blue ring (--brand-primary-500)
  • 2px width
  • 1px offset from element
  • Smooth transition

Color Contrast

All brand themes meet WCAG AA contrast requirements:
TokenLight ModeDark ModeRatio
--text-primary#1e293b on #ffffff#f8fafc on #1e293b12.6:1
--text-secondary#475569 on #ffffff#cbd5e1 on #1e293b7.2:1
--text-tertiary#64748b on #ffffff#94a3b8 on #1e293b4.9:1
All text passes WCAG AA (4.5:1) and most pass AAA (7:1).
ElementBackgroundContrast
Primary Button--interactive-primary✅ 4.7:1
Secondary Button--interactive-secondary✅ 3.2:1
Input Border--border-primary✅ 3.1:1
Focus Ring--border-focus✅ 3.8:1
All interactive elements meet 3:1 non-text contrast requirement.
StatusLightDarkContrast
Success#15803d on #f0fdf4#4ade80 on #14532d✅ 4.5:1+
Warning#b45309 on #fffbeb#fbbf24 on #78350f✅ 4.5:1+
Danger#b91c1c on #fef2f2#f87171 on #7f1d1d✅ 4.5:1+
Status messages use contrasting text on tinted backgrounds.

Touch Targets

All interactive elements meet the 44x44px minimum touch target size:
Touch Target Sizing
// Button sizes
--button-height-sm: 2rem;      /* 32px - with padding reaches 44px */
--button-height-md: 2.5rem;    /* 40px - with padding reaches 48px */
--button-height-lg: 3rem;      /* 48px - exceeds minimum */

// Input heights
--input-height-sm: 2.25rem;    /* 36px - with padding reaches 44px */
--input-height-md: 2.75rem;    /* 44px - meets minimum */
--input-height-lg: 3.25rem;    /* 52px - exceeds minimum */

// Checkbox/Radio
.checkbox-box {
  @apply w-5 h-5;              /* 20px visual */
  @apply p-2;                  /* Additional touch area = 44px */
}
All interactive components have sufficient padding/margin to meet the 44px touch target, even if the visual size appears smaller.

SSR Compatibility

Stride is fully compatible with server-side rendering:
import { SSRProvider } from 'react-aria-components';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <SSRProvider>
          {children}
        </SSRProvider>
      </body>
    </html>
  );
}
Why SSRProvider?
  • Ensures consistent IDs between server and client
  • Prevents hydration mismatches
  • Required for React Aria Components to work with SSR
Always wrap your app with SSRProvider when using server-side rendering. Without it, you may encounter hydration errors.

Semantic HTML

Components render semantic HTML5 elements:
// Renders as <button>
<Button onPress={handleClick}>
  Submit Form
</Button>

// Renders as <a>
<Button href="/about">
  Learn More
</Button>

// React Aria automatically chooses the right element

Form Accessibility

Forms are fully accessible with proper labels and validation:
<Input
  label="Email address"
  type="email"
  isRequired
  description="We'll never share your email"
  errorMessage="Please enter a valid email"
/>

// Generates:
// <label for="email-1">Email address *</label>
// <input id="email-1" type="email" aria-required="true" />
// <div id="email-1-desc">We'll never share your email</div>
// <div id="email-1-error" aria-live="polite">Invalid</div>

Animations & Motion

Animations respect prefers-reduced-motion:
Motion Preferences
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* Animations in the design system */
@keyframes accordion-down {
  from { height: 0; opacity: 0; }
  to { height: var(--accordion-content-height); opacity: 1; }
}

.animate-accordion-down {
  animation: accordion-down 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
Users who enable “Reduce Motion” in their OS will see instant transitions instead of animations.

Testing Accessibility

1

Keyboard Testing

Navigate your entire app using only the keyboard:
  • Tab through all interactive elements
  • Enter/Space to activate buttons
  • Arrow keys in menus and selects
  • Escape to close modals
2

Screen Reader Testing

Test with actual screen readers:
  • macOS: VoiceOver (Cmd + F5)
  • Windows: NVDA (free) or JAWS
  • iOS: VoiceOver (Settings > Accessibility)
  • Android: TalkBack (Settings > Accessibility)
3

Automated Testing

Use tools to catch common issues:
# Install axe DevTools browser extension
# Or use jest-axe for automated tests
npm install --save-dev jest-axe
import { axe } from 'jest-axe';

test('Button has no accessibility violations', async () => {
  const { container } = render(<Button>Click me</Button>);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
4

Color Contrast Testing

Use browser DevTools:
  • Chrome DevTools: Elements > Styles > Color picker shows contrast ratio
  • Firefox: Accessibility Inspector
  • Online: WebAIM Contrast Checker

Best Practices

// ❌ Bad - no accessible label
<Button><XIcon /></Button>

// ✅ Good - aria-label for icon-only buttons
<Button aria-label="Close dialog">
  <XIcon />
</Button>

// ✅ Good - visible text label
<Button>
  <XIcon /> Close
</Button>
// ❌ Bad - skips heading levels
<h1>Page Title</h1>
<h4>Section Title</h4>

// ✅ Good - proper hierarchy
<h1>Page Title</h1>
<h2>Section Title</h2>
<h3>Subsection</h3>
<Input
  label="Password"
  type="password"
  description="Must be at least 8 characters"
  errorMessage="Password is too short"
/>
// ❌ Bad - div soup
<div onClick={handleClick}>Click me</div>

// ✅ Good - semantic button
<Button onPress={handleClick}>Click me</Button>

Resources

React Aria Docs

Official React Aria Components documentation

WCAG 2.1 Guidelines

W3C Web Content Accessibility Guidelines

WAI-ARIA Patterns

ARIA Authoring Practices Guide

WebAIM Resources

Accessibility testing tools and guides

Next Steps

Component Library

Explore accessible components

Theming System

Learn how colors maintain contrast

Build docs developers (and LLMs) love