Accessible AI interfaces
Chat UIs break accessibility in three predictable ways: streams that never stop announcing, focus that jumps on every token, and controls that only work with a mouse. Slipstream handles the first two inside the components — this page covers what they do and what your app must still own.
What the components already do
| Concern | Built-in behavior |
|---|---|
| Streaming | aria-busy on the streaming message; polite completion announcement instead of per-token chatter. |
| Focus | Menus/popovers move focus in and restore on Escape; tooltips follow focus, not just hover. |
| Scroll | Auto-scroll only near the bottom; keyboard users are never yanked away from what they read. |
| Motion | Animations respect prefers-reduced-motion (including accordion expansion). |
| Errors | Retry cards + FormField errors render with role=alert so failures are announced. |
| Testing | Every component ships a jest-axe suite; CI runs them per package. |
What your app still owns
1. Announce user-visible status changes
Connection loss, saved states, queued messages — anything that changes outside the transcript needs one polite live region. Never make the transcript itself a live region.
{/* One status region for the whole surface */}<p role="status" aria-live="polite" className="sr-only">{statusMessage}</p>
2. Keep the composer reachable
- PromptInput is a real textarea: Enter sends, Shift+Enter newlines — document it in placeholder or hint text.
- Don't trap focus in the transcript; Tab order should reach the composer from anywhere.
- Icon-only buttons (send, stop, thumbs) need aria-labels — the tooltip supplements, never replaces.
3. Don't announce every token
The instinct is to mirror streaming output into a live region — that makes screen readers unusable for minutes. Announce only phases: generating… (optional) and response complete. Conversation does this; don't duplicate it.
4. Color and contrast are your tokens
Semantic status tokens (success/warning/info) ship as CSS variables with paired foregrounds — status pills pass AA in both themes by default. If you override tokens via createTheme, re-check contrast on destructive/success on your brand hues.
Testing checklist
import { axe } from 'jest-axe';it('chat surface has no accessibility violations', async () => {const { container } = render(<ChatSurface messages={transcript} />);expect(await axe(container)).toHaveNoViolations();});// Interaction coverage that matters for chat:// - keyboard: composer focus, suggestion buttons, stop button reachable// - streaming: aria-busy present while streamingMessageId is set// - errors: retry card announced (role/status) and retry reachable via keyboard
Also run an axe pass while streaming and with an error card open — the resting state alone misses half the surface. See Testing AI components for the full workflow, including manifest drift in CI.