assistant-ui accessibility
Add paced screen-reader updates to an assistant-ui thread without replaying messages already present in its history.
Install the assistant-ui adapter
npm install @generative-a11y/core @generative-a11y/dom @generative-a11y/assistant-uiUse the thread your app already owns. See Compatibility for supported host dependencies.
Connect an existing thread runtime
Create the binding
Add this function to your browser app. It creates a runtime and browser delivery, then subscribes to your existing thread.
import { createRuntime } from "@generative-a11y/core";
import { bindRuntime } from "@generative-a11y/dom";
import { bindThread, type ThreadRuntimeSource } from "@generative-a11y/assistant-ui";
export function connectAccessibility(thread: ThreadRuntimeSource) {
const runtime = createRuntime({});
const delivery = bindRuntime(runtime);
const binding = bindThread({
runtime,
scopeId: "support-thread",
thread,
});
return () => {
binding.dispose();
delivery.dispose();
runtime.dispose();
};
}Connect it to your app lifecycle
Call this once when your hydrated thread is ready. Here,
thread is your existing public assistant-ui thread runtime.
import { connectAccessibility } from "./accessibility/assistant-ui";
const disconnectAccessibility = connectAccessibility(thread);Keep the binding alive across responses. Call the returned function when the surface unmounts or before replacing the thread:
disconnectAccessibility();Keep one owner for each resource
The example owns its core runtime and DOM delivery. If your app already has
these, pass that runtime directly to bindThread and clean up only its returned
binding. In React, use the runtime from A11yProvider and return
() => binding.dispose() from your effect. Do not attach a second delivery
layer to that provider.
Keep scopeId stable and unique for each mounted thread. The adapter leaves your
visible UI and actions with the host application.
Optional configuration
maxTrackedEntities defaults to 1000 and bounds adapter identity records.
copy supplies localized tool and interaction labels. See the
bindThread reference for
all options and the localization guide for copy
configuration.
Compatibility
| Dependency | Supported range | Checked version |
|---|---|---|
| Node.js | 22+ | — |
@assistant-ui/core | >=0.3.13 <0.4.0 | 0.3.15 |
Your existing app supplies the host dependency. The adapter itself needs no React rendering layer; this example uses core and DOM directly.
Lifecycle mapping
| Public thread evidence | Reports |
|---|---|
| Append-only assistant text | Response text updates |
| Known complete or incomplete statuses | Completion, cancellation, or failure |
| Tool calls, results, approval IDs, and sources | Tool, approval, and citation updates |
bindThread reads only getState and subscribe. It records the initial
snapshot as history, so messages already present are not replayed. Connect after
history hydration to establish the intended baseline.
Expected screen-reader behavior
Core paces announcement intents; DOM writes them to hidden live regions. Routine streaming and status changes leave focus in place. Confirm spoken output with your supported screen readers and browsers: a DOM transcript records browser writes, not speech.
What public state cannot prove
General retries, connection changes, runs, steps, and run hierarchy are unavailable. Replay fidelity is partial. The adapter does not infer these events from rendering or private framework state.
Troubleshooting
- History is announced: bind after hydration and keep the thread identity and
scopeIdstable. - Repeated updates: check that only one binding observes the thread.
- Missing updates: verify the public thread state and supported dependency range before changing announcement policy.