generative-a11y
AI SDK

useChatAccessibility

Create the AI SDK observer and completion callbacks before calling useChat in the same React component.

Start here

For an existing compatible React + AI SDK app:

Terminal
npm install @generative-a11y/react @generative-a11y/ai-sdk

See the integration guide for peer versions, provider placement, preserved host options, and the preconstructed Chat case. The root observer alone does not provide browser delivery.

Complete useChat integration

Call useChatAccessibility first, pass chatCallbacks into useChat, then observe the documented snapshot.

components/chat.tsx
"use client";

import { DefaultChatTransport } from "ai";
import { useChat } from "@ai-sdk/react";
import { A11yProvider, useRuntime } from "@generative-a11y/react";
import {
  useChatAccessibility,
  useObserveChatAccessibility,
} from "@generative-a11y/ai-sdk/react";
import { useState } from "react";

function Chat() {
  const runtime = useRuntime();
  const accessibility = useChatAccessibility({
    runtime,
    scopeId: "support",
  });
  const chat = useChat({
    id: "support",
    transport: new DefaultChatTransport({ api: "/api/chat" }),
    ...accessibility.chatCallbacks,
  });
  useObserveChatAccessibility({ integration: accessibility, snapshot: chat });
  const [input, setInput] = useState("");

  // This is example host UI. Keep your existing message list and composer.
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        if (!input.trim()) return;
        void chat.sendMessage({ text: input });
        setInput("");
      }}
    >
      {chat.messages.map((message) => (
        <p key={message.id}>
          {message.parts
            .map((part) => (part.type === "text" ? part.text : ""))
            .join("")}
        </p>
      ))}
      <label>
        Message
        <input
          value={input}
          onChange={(event) => setInput(event.target.value)}
        />
      </label>
      <button
        type="submit"
        disabled={chat.status === "streaming" || chat.status === "submitted"}
      >
        Send
      </button>
    </form>
  );
}

export function App() {
  return (
    <A11yProvider>
      <Chat />
    </A11yProvider>
  );
}

Existing callbacks

Already use onFinish or onError? Pass your existing functions to useChatAccessibility so they are composed rather than replaced. Keep the rest of your options, including transport, in existingOptions, and spread the composed callbacks last. Omit either callback if your app does not use it.

components/chat.tsx
const accessibility = useChatAccessibility({
  runtime,
  scopeId: "support",
  onFinish,
  onError,
});
const chat = useChat({ ...existingOptions, ...accessibility.chatCallbacks });

Options

Pass these options to useChatAccessibility. The provider supplies runtime; keep scopeId stable for the mounted chat.

Prop

Type

useObserveChatAccessibility

useObserveChatAccessibility reads your ChatIntegration without disposing it.

Prop

Type

The example uses your existing AI SDK /api/chat endpoint. A11yProvider owns the runtime and browser delivery; useChatAccessibility owns only its observer. Both clean up on unmount. Keep your existing transport and UI. Add host onFinish/onError callbacks to useChatAccessibility when needed; it composes them with its own callbacks. Do not overwrite chatCallbacks after spreading them into useChat.

Public declarations: @generative-a11y/ai-sdk/react

All exported values and types for this entry point. Import these names from @generative-a11y/ai-sdk/react; the signatures below are reference material, not a replacement for the ownership and lifecycle guidance above.

ChatIntegration — type
interface ChatIntegration<UI_MESSAGE extends UIMessage = UIMessage> {
    readonly observer: ChatObserver;
    readonly chatCallbacks: {
        readonly onFinish: ChatOnFinishCallback<UI_MESSAGE>;
        readonly onError: ChatOnErrorCallback;
    };
}
useChatAccessibility — value
/**
 * Creates an observer and the callbacks that must be passed to `useChat()`.
 * Invoke this hook before `useChat()` in the same component.
 */
declare function useChatAccessibility<UI_MESSAGE extends UIMessage>(options: UseChatAccessibilityOptions<UI_MESSAGE>): ChatIntegration<UI_MESSAGE>;
UseChatAccessibilityOptions — type
interface UseChatAccessibilityOptions<UI_MESSAGE extends UIMessage = UIMessage> {
    readonly runtime: Pick<Runtime, "dispatch">;
    readonly scopeId: string;
    /** Captured with the observer; replace its scope/runtime to change language. */
    readonly copy?: AdapterCopy;
    readonly maxTrackedEntities?: number;
    readonly getToolLabel?: (context: ToolLabelContext) => string;
    readonly onFinish?: ChatOnFinishCallback<UI_MESSAGE>;
    readonly onError?: ChatOnErrorCallback;
}
UseChatSnapshot — type
/** The documented public state returned from `useChat()` that this hook reads. */
type UseChatSnapshot<UI_MESSAGE extends UIMessage = UIMessage> = Pick<UseChatHelpers<UI_MESSAGE>, "messages" | "status" | "error">;
useObserveChatAccessibility — value
/**
 * Observes a public `useChat()` snapshot after `useChat()` has been invoked.
 * It borrows the integration and deliberately never disposes its observer.
 */
declare function useObserveChatAccessibility<UI_MESSAGE extends UIMessage>(options: UseObserveChatAccessibilityOptions<UI_MESSAGE>): void;
UseObserveChatAccessibilityOptions — type
interface UseObserveChatAccessibilityOptions<UI_MESSAGE extends UIMessage = UIMessage> {
    readonly integration: ChatIntegration<UI_MESSAGE>;
    readonly snapshot: UseChatSnapshot<UI_MESSAGE>;
}