Accessibility
Configure keyboard navigation, focus trapping, screen reader support, and reduced motion for WCAG 2.1 AA compliant tours
Accessibility
User Tour Kit is built with accessibility as a core principle, not an afterthought. Every component follows WCAG 2.1 AA guidelines to ensure your tours work for all users, including those using screen readers, keyboard navigation, or who have motion sensitivity.
User Tour Kit targets a Lighthouse Accessibility Score of 100 and full WCAG 2.1 AA compliance.
Why Accessibility Matters for Tours
Product tours and onboarding flows often interrupt the normal page experience. Without proper accessibility:
- Screen reader users won't understand what's happening when a tour starts
- Keyboard users can get trapped or lose their place in the page
- Users with motion sensitivity may experience discomfort from animations
- Users with cognitive disabilities may struggle with complex multi-step flows
User Tour Kit handles all of these concerns automatically.
Focus Management
When a tour step activates, focus needs careful management to ensure keyboard users can interact with the tour and return to their previous location when done.
How Focus Trap Works
User Tour Kit's useFocusTrap hook automatically:
- Saves the previously focused element before the tour starts
- Traps Tab/Shift+Tab within the tour card
- Wraps focus from last to first element (and vice versa)
- Restores focus to the original element when the tour ends
import { useFocusTrap } from '@tour-kit/core'
function TourCard({ isActive }) {
const { containerRef, activate, deactivate } = useFocusTrap(isActive)
useEffect(() => {
if (isActive) {
activate()
} else {
deactivate()
}
}, [isActive, activate, deactivate])
return (
<div ref={containerRef} role="dialog" aria-modal="true">
{/* Tour content */}
</div>
)
}Focusable Elements
The focus trap identifies focusable elements using this selector:
const FOCUSABLE_SELECTOR = [
'a[href]:not([tabindex="-1"])',
'button:not([disabled]):not([tabindex="-1"])',
'textarea:not([disabled]):not([tabindex="-1"])',
'input:not([disabled]):not([tabindex="-1"])',
'select:not([disabled]):not([tabindex="-1"])',
'[tabindex]:not([tabindex="-1"])',
].join(', ')Avoid using tabindex="-1" on interactive elements inside tour cards, as they will be excluded from the focus trap.
Using Focus Trap in Custom Components
If you're building headless components, you can use useFocusTrap directly:
import { useFocusTrap } from '@tour-kit/core'
function MyCustomCard({ isActive, children }) {
const { containerRef, activate, deactivate } = useFocusTrap(isActive)
useEffect(() => {
if (isActive) {
// Small delay ensures DOM is ready
requestAnimationFrame(() => activate())
}
return () => deactivate()
}, [isActive, activate, deactivate])
return (
<div
ref={containerRef}
role="dialog"
aria-modal="true"
aria-labelledby="tour-title"
>
<h2 id="tour-title">Welcome</h2>
{children}
<button>Next</button>
<button>Close</button>
</div>
)
}Keyboard Navigation
User Tour Kit provides intuitive keyboard controls out of the box.
Default Key Bindings
| Key | Action |
|---|---|
ArrowRight | Go to next step |
Enter | Go to next step |
ArrowLeft | Go to previous step |
Escape | Exit the tour |
Tab | Move focus within tour card |
Shift+Tab | Move focus backwards within tour card |
Smart Input Detection
Keyboard navigation is automatically disabled when users are typing in form fields:
// Internal logic - keyboard events are ignored when focus is in:
// - <input> elements
// - <textarea> elements
// This prevents arrow keys from interfering with text editingCustomizing Keyboard Controls
You can customize key bindings when setting up your tour:
import { TourProvider } from '@tour-kit/react'
function App() {
return (
<TourProvider
tour={myTour}
keyboardConfig={{
enabled: true,
nextKeys: ['ArrowRight', 'Enter', 'Space'],
prevKeys: ['ArrowLeft', 'Backspace'],
exitKeys: ['Escape', 'q'],
}}
>
{children}
</TourProvider>
)
}Disabling Keyboard Navigation
For tours with form inputs or interactive content, you may want to disable certain keys:
<TourProvider
tour={formTour}
keyboardConfig={{
enabled: true,
nextKeys: ['Enter'], // Only Enter advances
prevKeys: [], // Disable going back
exitKeys: ['Escape'],
}}
>Screen Reader Support
User Tour Kit ensures screen reader users understand what's happening at every step.
ARIA Attributes
All tour cards include proper ARIA attributes:
<div
role="dialog"
aria-modal="true"
aria-labelledby="tour-step-title-welcome"
>
<h3 id="tour-step-title-welcome">Welcome to the App</h3>
<p>Let's get you started...</p>
</div>| Attribute | Purpose |
|---|---|
role="dialog" | Announces the tour card as a dialog |
aria-modal="true" | Indicates content behind is inert |
aria-labelledby | Links to the step title for announcement |
Live Announcements
When tour steps change, User Tour Kit announces the new step to screen readers:
import { announce, getStepAnnouncement } from '@tour-kit/core'
// When step changes:
const message = getStepAnnouncement(step.title, currentIndex + 1, totalSteps)
// Returns: "Step 2 of 5: Feature Overview"
announce(message, 'polite')The announcement uses a visually hidden element with aria-live="polite":
// Created dynamically, visually hidden but announced
<div
role="status"
aria-live="polite"
aria-atomic="true"
style={{
position: 'absolute',
width: '1px',
height: '1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
}}
>
Step 2 of 5: Feature Overview
</div>Progress Indicators
Progress components include proper ARIA for screen readers:
<span aria-label="Step 2 of 5">
2 of 5
</span><div
role="progressbar"
aria-valuenow={2}
aria-valuemin={1}
aria-valuemax={5}
aria-label="Step 2 of 5"
>
<div style={{ width: '40%' }} />
</div><div role="group" aria-label="Step 2 of 5">
<div aria-current={undefined} />
<div aria-current="step" /> {/* Current step */}
<div aria-current={undefined} />
</div>Icon Buttons
All icon-only buttons include descriptive labels:
<button type="button" aria-label="Close tour">
<svg aria-hidden="true">
{/* X icon */}
</svg>
</button>Always use aria-hidden="true" on decorative SVG icons to prevent screen readers from announcing them.
Reduced Motion Support
Users who experience motion sickness or have vestibular disorders can enable "reduced motion" in their operating system. User Tour Kit respects this preference.
Automatic Detection
User Tour Kit automatically detects the prefers-reduced-motion media query:
import { usePrefersReducedMotion } from '@tour-kit/core'
function MyComponent() {
const prefersReducedMotion = usePrefersReducedMotion()
return (
<div style={{
transition: prefersReducedMotion ? 'none' : 'all 0.3s ease'
}}>
Content
</div>
)
}What Changes with Reduced Motion
When reduced motion is enabled:
| Feature | Normal | Reduced Motion |
|---|---|---|
| Overlay transitions | Fade animation | Instant show/hide |
| Card entrance | Slide + fade | Instant appear |
| Spotlight movement | Animated | Instant move |
| GIF players | Auto-play | Paused by default |
| Lottie animations | Playing | Static frame |
| Video content | Auto-play | Shows poster image |
Media Fallbacks
For animated media, provide a static fallback image:
import { TourMedia } from '@tour-kit/media'
<TourMedia
src="/videos/feature-demo.mp4"
alt="Feature demonstration"
poster="/images/feature-poster.jpg"
reducedMotionFallback="/images/feature-static.jpg"
/>When prefers-reduced-motion: reduce is active, the static image displays instead of the video.
GIF Player Behavior
The GIF player pauses automatically and provides a play button:
// GIF respects reduced motion preference
<GifPlayer
src="/animations/loading.gif"
alt="Loading indicator"
poster="/images/loading-frame.png"
autoplay={true} // Ignored if reduced motion is preferred
/>Users can manually play the GIF using the play button, which includes proper ARIA:
<button
aria-label={isPlaying ? "Pause Loading indicator" : "Play Loading indicator"}
aria-pressed={isPlaying}
>
{/* Play/Pause icon */}
</button>Testing Reduced Motion
To test reduced motion in your browser:
- Open DevTools (F12)
- Press Cmd/Ctrl + Shift + P
- Type "reduced motion"
- Select "Emulate CSS prefers-reduced-motion: reduce"
- Go to
about:config - Search for
ui.prefersReducedMotion - Set value to
1
- System Preferences > Accessibility
- Display > Reduce motion
- Or use Web Inspector's media query simulation
Touch Targets
Interactive elements in User Tour Kit meet minimum touch target requirements:
/* Minimum 44x44px touch target for buttons */
.tour-button {
min-width: 44px;
min-height: 44px;
}Focus Indicators
All interactive elements have visible focus indicators:
/* Focus ring styling */
.tour-button:focus-visible {
outline: 2px solid var(--tour-focus-ring);
outline-offset: 2px;
}Never remove focus indicators with outline: none without providing an alternative visible focus state.
Color Contrast
User Tour Kit uses CSS variables that ensure sufficient color contrast:
:root {
--tour-card-background: hsl(0 0% 100%);
--tour-card-foreground: hsl(222 47% 11%);
--tour-overlay-background: hsl(0 0% 0% / 0.5);
}Overlay Considerations
The overlay uses a maximum opacity of 50% to ensure content behind remains somewhat visible:
.tour-overlay {
background: hsl(0 0% 0% / 0.5); /* Max 50% opacity */
}This helps users maintain orientation within the page during the tour.
Testing Your Tours
Automated Testing
Use vitest-axe for automated accessibility testing:
import { render } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
import { TourCard } from '@tour-kit/react'
expect.extend(toHaveNoViolations)
test('TourCard has no accessibility violations', async () => {
const { container } = render(
<TourCard
title="Welcome"
description="Let's get started"
isActive={true}
/>
)
const results = await axe(container)
expect(results).toHaveNoViolations()
})Manual Testing Checklist
Before shipping your tour, verify:
- Keyboard only: Complete the entire tour using only keyboard
- Screen reader: Test with VoiceOver (Mac), NVDA, or JAWS
- Reduced motion: Enable reduced motion and verify no jarring animations
- Focus visible: Tab through and ensure focus ring is always visible
- High contrast: Test with Windows High Contrast Mode
- Zoom: Test at 200% zoom level
Screen Reader Testing
| Screen Reader | Platform | Free? |
|---|---|---|
| VoiceOver | macOS, iOS | Yes |
| NVDA | Windows | Yes |
| JAWS | Windows | No |
| TalkBack | Android | Yes |
Quick VoiceOver test (Mac):
- Press Cmd + F5 to enable VoiceOver
- Use Tab to navigate to your tour trigger
- Activate the tour
- Listen for step announcements
- Navigate using arrow keys
- Verify focus returns when tour closes
Accessibility Rules Summary
| Requirement | Implementation |
|---|---|
| Focus trap in dialogs | useFocusTrap hook |
| Keyboard navigation | useKeyboardNavigation hook |
| Screen reader announcements | aria-live regions |
| Dialog semantics | role="dialog" + aria-modal |
| Reduced motion | usePrefersReducedMotion hook |
| Focus indicators | CSS :focus-visible |
| Touch targets | Minimum 44x44px |
| Color contrast | WCAG AA ratios |