import { useAriaLiveAnnouncer, useTranslationContext } from "stream-chat-react";
const ExportButton = () => {
const announce = useAriaLiveAnnouncer();
const { t } = useTranslationContext();
const onExport = async () => {
await exportConversation();
announce(t("Conversation exported"));
};
return <button onClick={onExport}>{t("Export")}</button>;
};Custom Announcements
The SDK announces built-in events (incoming messages, notifications, Giphy actions, poll changes, search counts) to screen readers automatically. This recipe shows how to announce your own events, how to make announcements work inside custom modals, and how to reshape notification announcements.
For the model behind these APIs see the Accessibility guide.
Announcing a custom event
Use useAriaLiveAnnouncer for ad-hoc, already-localized messages. It returns a bare announce function and must be called inside Chat.
announce(message, options) takes priority ('polite' by default, 'assertive' to interrupt), delayMs (defer so the message lands after a competing focus/typing read-out), and dedupeMs (announce an identical message at most once per window). It returns a cancel function; for a delayed announcement, call it from effect cleanup so a closing surface does not speak after it unmounts.
import { useEffect } from "react";
import { useAriaLiveAnnouncer } from "stream-chat-react";
const UploadStatus = ({ isUploading }: { isUploading: boolean }) => {
const announce = useAriaLiveAnnouncer();
useEffect(() => {
if (!isUploading) return;
const cancel = announce("Uploading attachment", { delayMs: 300 });
return cancel; // do not announce if the component unmounts before the delay elapses
}, [announce, isUploading]);
return null;
};Reusing the built-in interaction registry
If your custom component triggers one of the SDK's known interactions, prefer useInteractionAnnouncements. Each entry is already localized (via the aria/* keys) with a sensible priority and delivery policy, so you announce by intent rather than composing strings.
import { useInteractionAnnouncements } from "stream-chat-react";
const CustomChannelPreview = ({ channel, setActiveChannel }) => {
const { announceInteraction } = useInteractionAnnouncements();
const onSelect = () => {
setActiveChannel(channel);
announceInteraction("channel.opened", {
name: channel.data?.name ?? "channel",
});
};
return <button onClick={onSelect}>{channel.data?.name}</button>;
};For a rapidly-updating value (such as a live result count), pass a per-call debounceMs so only the settled value is spoken, and call cancelInteraction when the source is torn down:
announceInteraction("search.resultCount", { count, debounceMs: 800 });
// later, when leaving search:
cancelInteraction("search.resultCount");Announcing inside a custom modal
Assistive technologies suppress live regions outside the active aria-modal subtree, so a message announced from inside your own modal would be silently dropped. Render an AriaLiveOutlet with a higher layer inside the modal; the SDK's announcer automatically routes announcements to it while it is mounted.
import { AriaLiveOutlet, useAriaLiveAnnouncer } from "stream-chat-react";
const ConfirmDialog = ({ onConfirm }: { onConfirm: () => void }) => {
const announce = useAriaLiveAnnouncer();
return (
<div role="dialog" aria-modal="true" aria-label="Confirm">
<button
onClick={() => {
onConfirm();
announce("Saved"); // heard because the outlet below is inside the modal
}}
>
Save
</button>
{/* Layer above the root outlet (0). */}
<AriaLiveOutlet layer={1} />
</div>
);
};Customizing notification announcements
Notifications are distinct from chat messages: they are entries your code adds to the notification system through the addNotification / addSystemNotification functions returned by useNotificationApi. NotificationAnnouncer reacts to those registered notifications and announces each new one (it does not watch message.new).
NotificationAnnouncer is rendered by Chat and read from the ComponentContext. Because Chat consumes it at its own level, provide your override with a ComponentProvider that wraps Chat. Use it to filter which notifications are announced and to reshape the spoken text.
import {
Chat,
ComponentProvider,
NotificationAnnouncer,
} from "stream-chat-react";
const CustomNotificationAnnouncer = () => (
<NotificationAnnouncer
// Announce only warnings and errors; skip informational notifications.
notificationFilter={(notification) => notification.severity !== "info"}
// Reshape the message. `defaultMessage` includes the severity prefix; `translatedMessage`
// is the localized body.
buildNotificationAnnouncement={({
defaultMessage,
notification,
translatedMessage,
}) =>
notification.severity === "error"
? `Error: ${translatedMessage}`
: defaultMessage
}
/>
);
export const App = ({ client, children }) => (
<ComponentProvider
value={{ NotificationAnnouncer: CustomNotificationAnnouncer }}
>
<Chat client={client}>{children}</Chat>
</ComponentProvider>
);Error-severity notifications are announced with assertive priority (they interrupt); all others are polite. Return an empty string from buildNotificationAnnouncement to suppress a notification without changing your filter.