Accessibility Utilities
Accessibility utilities: screen reader announcements, live regions, and ARIA attribute helpers for product tours
Accessibility Utilities
Utilities for building accessible tours. Includes screen reader announcements, unique ID generation, and motion preferences.
Why Use These Utilities?
Accessible tours require:
- Screen reader support - Announce step changes to assistive technology
- Unique IDs - ARIA relationships (
aria-describedby,aria-labelledby) - Motion respect - Honor user preferences for reduced motion
announce
Announce a message to screen readers using an ARIA live region.
Usage
import { announce } from '@tour-kit/core';
// Polite announcement (default)
announce('Step 2 of 5: Dashboard Overview');
// Assertive announcement (interrupts current speech)
announce('Tour completed!', 'assertive');Parameters
Prop
Type
Politeness Levels
| Level | Behavior | Use Case |
|---|---|---|
polite | Waits for current speech to finish | Step changes, progress updates |
assertive | Interrupts current speech | Errors, completion, urgent messages |
How It Works
- Creates a visually hidden
<div>withrole="status"andaria-live - Appends to
document.body - Sets content after 100ms delay (allows screen reader registration)
- Removes element after 1000ms
// Internal implementation
function announce(message: string, politeness = 'polite'): void {
const announcer = document.createElement('div');
announcer.setAttribute('role', 'status');
announcer.setAttribute('aria-live', politeness);
announcer.setAttribute('aria-atomic', 'true');
// ... visually hidden styles
document.body.appendChild(announcer);
setTimeout(() => { announcer.textContent = message; }, 100);
setTimeout(() => { document.body.removeChild(announcer); }, 1000);
}The 100ms delay is necessary because screen readers need time to register the live region before content is added.
getStepAnnouncement
Generate a standardized step announcement string.
Usage
import { getStepAnnouncement } from '@tour-kit/core';
// With step title
const msg1 = getStepAnnouncement('Dashboard Overview', 2, 5);
// "Step 2 of 5: Dashboard Overview"
// Without step title
const msg2 = getStepAnnouncement(undefined, 2, 5);
// "Step 2 of 5"Parameters
Prop
Type
Return Value
string - Formatted announcement message.
generateId
Generate a unique ID for ARIA relationships.
Usage
import { generateId } from '@tour-kit/core';
// Default prefix
const id1 = generateId();
// "tourkit-x7f8k2m9q"
// Custom prefix
const id2 = generateId('tooltip');
// "tooltip-a3b5c7d9e"Parameters
Prop
Type
Use Case: ARIA Relationships
function TourStep({ title, content }) {
const titleId = useMemo(() => generateId('title'), []);
const contentId = useMemo(() => generateId('content'), []);
return (
<div
role="dialog"
aria-labelledby={titleId}
aria-describedby={contentId}
>
<h2 id={titleId}>{title}</h2>
<p id={contentId}>{content}</p>
</div>
);
}prefersReducedMotion
Check if the user prefers reduced motion.
Usage
import { prefersReducedMotion } from '@tour-kit/core';
if (prefersReducedMotion()) {
// Skip animations
element.style.transition = 'none';
} else {
// Use animations
element.style.transition = 'opacity 300ms ease';
}Return Value
boolean - true if user has enabled reduced motion preference.
WCAG Requirement
WCAG 2.1 Success Criterion 2.3.3 requires respecting prefers-reduced-motion. Always check this before animating.
Complete Accessible Tour Example
import {
announce,
getStepAnnouncement,
generateId,
prefersReducedMotion,
} from '@tour-kit/core';
function AccessibleTourCard({ step, currentIndex, totalSteps }) {
const titleId = useMemo(() => generateId('step-title'), []);
const contentId = useMemo(() => generateId('step-content'), []);
// Announce step changes
useEffect(() => {
const announcement = getStepAnnouncement(
step.title,
currentIndex + 1,
totalSteps
);
announce(announcement);
}, [currentIndex, step.title, totalSteps]);
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={contentId}
className={prefersReducedMotion() ? '' : 'animate-fade-in'}
>
<h2 id={titleId}>{step.title}</h2>
<p id={contentId}>{step.content}</p>
<p className="sr-only">
Step {currentIndex + 1} of {totalSteps}
</p>
<nav aria-label="Tour navigation">
<button onClick={prev} disabled={currentIndex === 0}>
Previous
</button>
<button onClick={next}>
{currentIndex === totalSteps - 1 ? 'Finish' : 'Next'}
</button>
</nav>
</div>
);
}Screen Reader Testing
VoiceOver (macOS)
- Enable:
Cmd + F5 - Navigate:
Ctrl + Option + Arrow keys - Listen for step announcements
NVDA (Windows)
- Enable:
Ctrl + Alt + N - Navigate:
Taband arrow keys - Check announcement timing
Expected Behavior
| Action | Announcement |
|---|---|
| Step change | "Step X of Y: Title" |
| Tour complete | "Tour completed!" (assertive) |
| Focus trap | Focus stays within tour card |
ARIA Attributes Reference
// Tour container
<div
role="dialog" // Announces as dialog
aria-modal="true" // Indicates modal behavior
aria-labelledby={id} // Links to title
aria-describedby={id} // Links to content
>
// Navigation
<nav aria-label="Tour navigation">
<button aria-label="Go to previous step">Previous</button>
<button aria-label="Go to next step">Next</button>
</nav>
// Progress
<div role="status" aria-live="polite">
Step 2 of 5
</div>
// Close button
<button aria-label="Close tour">×</button>Related
- useFocusTrap - Focus management
- useKeyboardNavigation - Keyboard support
- useMediaQuery -
usePrefersReducedMotionhook - Accessibility Guide - Full WCAG compliance