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

ConcernBuilt-in behavior
Streamingaria-busy on the streaming message; polite completion announcement instead of per-token chatter.
FocusMenus/popovers move focus in and restore on Escape; tooltips follow focus, not just hover.
ScrollAuto-scroll only near the bottom; keyboard users are never yanked away from what they read.
MotionAnimations respect prefers-reduced-motion (including accordion expansion).
ErrorsRetry cards + FormField errors render with role=alert so failures are announced.
TestingEvery 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.

tsx
{/* 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

tsx
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.

Was this page helpful?