Accessibility
The SDK's components work with a keyboard and a screen reader: the composer, the channel and thread lists, search, menus and dialogs. Most of it is on by default. This page covers what you get, the parts you can reuse in your own components, and how to change what screen readers hear.
What you get out of the box
Rendering the components inside Chat gives you, with no setup:
- Announcements. Screen readers hear what happens out of sight: new messages, notifications, a Giphy sent, a poll created, the number of search results or suggestions, the channel or thread just opened, a change of audio playback speed, and each step of a voice recording.
- Keyboard navigation. In the channel list, the thread list and search results, the arrow keys move between rows, and
HomeandEndjump to the first and last. The arrow keys also move through the suggestion list and menus. - Focus management. Dialogs, menus and the Giphy preview keep track of where focus was before they opened, and put it back when they close.
- One name per list row. Each channel and thread row has one accessible name, such as "General. 3 unread messages. Last message from Alice: see you soon", instead of every element inside it read one by one.
- The view selector.
ChatView's channels and threads buttons sit in a navigation landmark, and the button of the view shown is markedaria-current. The threads button includes the unread thread count in its label.
Nothing changes for sighted mouse users.
How announcements are wired up
All announcements go through one announcer. Chat renders an AriaLiveAnnouncerProvider and one AriaLiveOutlet at its root, so any component inside it can announce. You don't render these yourself:
// What Chat renders around your app, simplified.
<AriaLiveAnnouncerProvider>
{/* your app */}
<AriaLiveOutlet portal />
</AriaLiveAnnouncerProvider>The provider holds the announcements and the announce() function. An outlet renders the visually hidden live regions, one polite and one assertive. Only one outlet renders at a time: the one with the highest layer, and among equal layers the one mounted last.
That matters for modals. Screen readers ignore live regions outside an open aria-modal dialog, so the SDK's modals and modal dialogs render an outlet of their own, with layer={1}. While one is open, announcements land inside it. When it closes, the root outlet takes over, and the announcements still in the dialog's live region are cleared, so the root outlet doesn't read them again.
Outside an AriaLiveAnnouncerProvider, announcing does nothing, so components that announce still work without Chat.
AriaLiveOutlet prop |
Default | What it does |
|---|---|---|
layer |
0 |
the outlet's layer. An outlet inside an aria-modal dialog needs a higher layer than the root's |
portal |
false |
renders the live regions in a portal on document.body. For the root outlet only: one inside a modal must stay inside its subtree |
Announcing to assistive technology
Two hooks announce, both inside Chat:
useAriaLiveAnnouncer()returnsannounce(message, options), for a message you write and translate yourself.useInteractionAnnouncements()announces the SDK's own interactions by name, already translated and timed.
Announcing a custom event
An export button tells screen-reader users when the export is done:
import { useAriaLiveAnnouncer } from "stream-chat-react";
export const ExportButton = ({
exportConversation,
}: {
exportConversation: () => Promise<void>;
}) => {
const announce = useAriaLiveAnnouncer();
const onExport = async () => {
await exportConversation();
announce("Conversation exported");
};
return (
<button onClick={onExport} type="button">
Export
</button>
);
};announce takes these options:
| Option | What it does |
|---|---|
priority |
'polite', the default, waits for the screen reader to finish. 'assertive' interrupts it: keep it for what can't wait |
delayMs |
waits this long before announcing, so the message comes after something the screen reader reads first, such as the newly focused field |
dedupeMs |
announces the same message at most once in this window, for example when an effect runs twice in development |
announce returns a function that cancels a delayed announcement. Return it from an effect, so a component that unmounts before the delay doesn't speak:
import { useEffect } from "react";
import { useAriaLiveAnnouncer } from "stream-chat-react";
export const UploadStatus = ({ isUploading }: { isUploading: boolean }) => {
const announce = useAriaLiveAnnouncer();
useEffect(() => {
if (!isUploading) return;
// Cancelled if the upload finishes or the component unmounts within 300 ms.
return announce("Uploading attachment", { delayMs: 300 });
}, [announce, isUploading]);
return null;
};The message is spoken as you pass it. Translate it first, as in Localizing announcements.
Reusing the built-in interaction registry
When your component does something the SDK already announces, announce it by name. A channel row of your own that opens a channel confirms it as the default row does:
import type { Channel } from "stream-chat";
import {
useChannelPreviewInfo,
useInteractionAnnouncements,
useWorkspaceNavigation,
} from "stream-chat-react";
export const ChannelLink = ({ channel }: { channel: Channel }) => {
const { displayTitle = "" } = useChannelPreviewInfo({ channel });
const { announceInteraction } = useInteractionAnnouncements();
const { openChannel } = useWorkspaceNavigation();
const open = () => {
openChannel(channel);
// Read 1.5 seconds later, after the screen reader reads the focused composer.
announceInteraction("channel.opened", { name: displayTitle });
};
return (
<button onClick={open} type="button">
{displayTitle}
</button>
);
};The registry:
| Interaction | Parameters | Priority | Delivery |
|---|---|---|---|
channel.opened |
{ name } |
polite | after 1.5 s |
thread.opened |
{ name } |
polite | after 1.5 s |
command.selected |
{ command } |
polite | after 1.5 s |
user.selected |
{ user } |
polite | at once |
giphy.sent |
none | polite | at once |
giphy.canceled |
none | polite | at once |
giphy.shuffled |
{ title? } |
polite | at once |
poll.dialogOpened |
none | assertive | at once, once per second |
poll.optionPickedUp |
{ option } |
assertive | at once |
poll.optionDropped |
{ option, position } |
assertive | at once |
poll.optionRemoved |
{ option } |
polite | at once |
poll.sent |
none | polite | at once |
search.resultCount |
{ count, allResultsLoaded? } |
polite | when it settles for 1 s |
search.cleared |
none | polite | at once |
suggestions.count |
{ count, suggestionsLabel? } |
polite | when it settles for 500 ms |
audioPlayer.playbackRateChanged |
{ rate } |
polite | at once, once per 300 ms |
voiceRecording.started |
none | polite | at once |
voiceRecording.paused |
none | polite | at once |
voiceRecording.resumed |
none | polite | at once |
voiceRecording.attached |
none | polite | at once |
voiceRecording.sent |
none | polite | at once |
The "opened" interactions wait 1.5 seconds because opening a channel moves focus to the composer, and the screen reader reads the composer first. The wait survives the component unmounting, since opening a channel can hide the list the row was in. A second call from the same component within the wait replaces the first.
An interaction that takes parameters also takes two timing options next to them:
debounceMswaits until calls stop for this long, and announces only the last. Use it for a value that keeps changing, such as a live count. It's cancelled when the component unmounts.delayMswaits this long, then announces. It isn't cancelled when the component unmounts.
debounceMs wins when both are set. cancelInteraction(name) drops a pending announcement, or all of them without a name:
announceInteraction("search.resultCount", { count, debounceMs: 800 });
// When the user leaves search:
cancelInteraction("search.resultCount");Announcing inside a custom modal
A dialog of your own with aria-modal="true" needs its own outlet, or announcements made while it's open are lost. Render one inside it with a layer above the root's:
import { AriaLiveOutlet, useAriaLiveAnnouncer } from "stream-chat-react";
export const ConfirmDialog = ({ onConfirm }: { onConfirm: () => void }) => {
const announce = useAriaLiveAnnouncer();
return (
<div aria-label="Confirm" aria-modal="true" role="dialog">
<button
onClick={() => {
onConfirm();
// Heard, because the outlet below is inside the dialog.
announce("Saved");
}}
type="button"
>
Save
</button>
{/* Above the root outlet's layer, 0. Not portalled, so it stays inside the dialog. */}
<AriaLiveOutlet layer={1} />
</div>
);
};The SDK's own dialogs, such as those opened through GlobalModal, already do this.
Customizing notification announcements
Chat renders NotificationAnnouncer, which reads new notifications aloud, as Notification announcements describes. Replace it to choose which notifications it reads and what it says. Chat reads it from the components above it, so pass the override with WithComponents around Chat:
import type { PropsWithChildren } from "react";
import { Chat, NotificationAnnouncer, WithComponents } from "stream-chat-react";
import type {
NotificationAnnouncementBuilder,
NotificationAnnouncementFilter,
} from "stream-chat-react";
// Skip informational notifications.
const notificationFilter: NotificationAnnouncementFilter = (notification) =>
notification.severity !== "info";
// Errors start with "Error:", the rest keep the default wording.
const buildNotificationAnnouncement: NotificationAnnouncementBuilder = ({
defaultMessage,
notification,
translatedMessage,
}) =>
notification.severity === "error"
? `Error: ${translatedMessage}`
: defaultMessage;
const CustomNotificationAnnouncer = () => (
<NotificationAnnouncer
buildNotificationAnnouncement={buildNotificationAnnouncement}
notificationFilter={notificationFilter}
/>
);
const overrides = { NotificationAnnouncer: CustomNotificationAnnouncer };
export const App = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>
<Chat client={client}>{children}</Chat>
</WithComponents>
);The builder receives:
translatedMessage: the notification's text, translated as the notification itself shows it;defaultMessage: what the announcer says by default, the translated text after a severity prefix, such as "Error notification: Error adding flag";notification: the notification.
Return an empty string to say nothing for a notification.
Focus and visibility utilities
useFocusReturn
useFocusReturn() returns reserve and restore, for a surface that takes focus and gives it back. Call reserve() before you move focus into the surface: it remembers the focused element, such as the composer. Call restore() when the surface is done: focus goes back there, if that element is still in the page.
import { useRef } from "react";
import type { KeyboardEvent } from "react";
import { useFocusReturn } from "stream-chat-react";
export const EmojiPanel = () => {
const { reserve, restore } = useFocusReturn();
const panelRef = useRef<HTMLDivElement>(null);
const open = () => {
// Remembers the focused element, then moves focus into the panel.
reserve();
panelRef.current?.focus();
};
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
// Escape gives focus back to where it was.
if (event.key === "Escape") restore();
};
return (
<>
<button onClick={open} type="button">
Pick an emoji
</button>
<div onKeyDown={onKeyDown} ref={panelRef} tabIndex={-1}>
{/* the emoji grid */}
</div>
</>
);
};reserve({ target }) remembers another element instead of the focused one. restore() moves focus at once, so delay an announcement yourself if it should come after the focused element is read.
useInertWhenHidden
useInertWhenHidden(hidden) takes a control that stays mounted while hidden out of the tab order and away from screen readers. It returns props to spread on the element: while hidden is true, they set aria-hidden, inert, tabIndex={-1} and hidden; otherwise they set nothing.
import { useInertWhenHidden } from "stream-chat-react";
export const Toolbar = ({ collapsed }: { collapsed: boolean }) => {
const inertProps = useInertWhenHidden(collapsed);
return (
<button {...inertProps} type="button">
Add attachment
</button>
);
};The hidden attribute sets display: none, which stops a CSS transition. For an element hidden by a transition, pass { setHiddenAttribute: false }: the element stays laid out, and is still out of the tab order and away from screen readers. The composer does this for its extra buttons while a command is active.
A control that isn't available is best not rendered at all. Use useInertWhenHidden only for one that has to stay mounted, to keep its state, its measurements or its transition.
The hook writes inert in the form your React version expects, 17, 18 or 19, so React logs no warning.
VisuallyHidden
VisuallyHidden renders a span that screen readers read and nobody sees, for text such as a label for an icon-only control.
Incoming message announcements
MessageList and VirtualizedMessageList announce new messages from other users through useIncomingMessageAnnouncements:
- One message reads "New message from Bob". Several within a second read "3 new messages": the hook speaks at most once a second.
- It skips the user's own messages, and deleted, ephemeral, error, system, failed and still-sending ones.
- A channel's list announces messages in the channel, not replies. A thread's list announces replies in that thread.
It listens for message.new on the channel, not to the notification system. The hook is exported for a message list of your own:
import {
useChannel,
useChatContext,
useIncomingMessageAnnouncements,
} from "stream-chat-react";
// Render inside Channel, next to your own message list.
export const IncomingMessageAnnouncer = () => {
const channel = useChannel();
const { client } = useChatContext();
useIncomingMessageAnnouncements({ channel, ownUserId: client.userID });
return null;
};For a thread's list, pass threadList: true and the thread's id as activeThreadId.
Notification announcements
Notifications are the messages the SDK and your code publish to client.notifications, for example with addNotification from useNotificationApi(). Notifications covers them.
NotificationAnnouncer reads each new one aloud:
- An error interrupts, and every other severity waits its turn.
- By default it says the severity first: "Success notification: Message pinned", "Error notification: Error adding flag". The text after the prefix is translated, the prefix isn't.
- Notifications already in the list when it mounts aren't read.
- It reads every notification, including those no list shows, such as the lost-connection system notification, and those a display filter hides.
The Notification component's message is also a live region: assertive for errors and polite otherwise. addNotification takes ariaLive to change that for one notification. NotificationAnnouncer ignores it and goes by severity.
Customizing notification announcements shows how to choose and reword what's read.
Customizing accessible names for list rows
How the label is built
A channel row's accessible name is built from named parts, joined in an order. ChannelListItemUI and ThreadListItemUI take an accessibleLabelConfig prop that changes any of it:
partsis merged over the default parts: a new name adds a part, an existing name replaces it.orderdecides which parts are read, and in what order. A part not inorderis never read, even if it's defined.separatorjoins the parts that aren't empty. It defaults to". ".buildreturns the whole label, and ignores the rest.
The default parts are read in this order:
- channel rows:
name,active,unreadCount,lastMessage(sender and preview),attachments,linkPreview,deliveryStatus,time; - thread rows:
name(the channel),active,unreadCount,parentMessage,replyCount,time.
A part returns its text, or undefined to be left out.
Adding custom parts to a channel row
Pass the config from your own ChannelListItemUI, set through WithComponents. This one reads "Pinned", "Muted" and "Frozen" after the name, each only when it's true:
import type { PropsWithChildren } from "react";
import {
asDynamicKey,
ChannelListItemUI,
DEFAULT_CHANNEL_LIST_ITEM_LABEL_ORDER,
WithComponents,
} from "stream-chat-react";
import type {
ChannelListItemLabelConfig,
ChannelListItemUIProps,
} from "stream-chat-react";
const accessibleLabelConfig: ChannelListItemLabelConfig = {
parts: {
frozen: ({ channel, t }) =>
channel.data?.frozen
? t(asDynamicKey("myApp.channelList.frozen"), "Frozen")
: undefined,
muted: ({ channel, t }) =>
channel.muteStatus().muted
? t(asDynamicKey("myApp.channelList.muted"), "Muted")
: undefined,
pinned: ({ channel, t }) =>
channel.state.membership?.pinned_at
? t(asDynamicKey("myApp.channelList.pinned"), "Pinned")
: undefined,
},
// The defaults, with the new parts after the name.
order: [
"name",
"pinned",
"muted",
"frozen",
...DEFAULT_CHANNEL_LIST_ITEM_LABEL_ORDER.slice(1),
],
};
const LabelledChannelListItemUI = (props: ChannelListItemUIProps) => (
<ChannelListItemUI {...props} accessibleLabelConfig={accessibleLabelConfig} />
);
const overrides = { ChannelListItemUI: LabelledChannelListItemUI };
export const WithChannelRowLabels = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);The label of a pinned, muted channel now starts "General. Pinned. Muted. 3 unread messages", and goes on with the default parts.
A part receives the row's data: the channel, active, unreadCount, latestMessage, displayTitle, the translation function t and more. Mute, pin and frozen state are read from the channel. The SDK has no "Pinned", "Muted" or "Frozen" text, so the parts use keys of their own, through asDynamicKey with the English text after the key. Localization explains how to translate them.
Reordering or dropping default parts
order alone decides what's read, so a shorter list makes a shorter label. This one reads the name, the unread count and the last message, and drops attachments, link previews, delivery status and time:
import type { ChannelListItemLabelConfig } from "stream-chat-react";
const accessibleLabelConfig: ChannelListItemLabelConfig = {
order: ["name", "unreadCount", "lastMessage"],
};Customizing thread rows
ThreadListItemUI works the same way, with ThreadListItemLabelConfig and DEFAULT_THREAD_LIST_ITEM_LABEL_ORDER. This adds the number of participants after the channel name:
import type { PropsWithChildren } from "react";
import {
asDynamicKey,
DEFAULT_THREAD_LIST_ITEM_LABEL_ORDER,
ThreadListItemUI,
WithComponents,
} from "stream-chat-react";
import type {
ThreadListItemLabelConfig,
ThreadListItemUIProps,
} from "stream-chat-react";
const accessibleLabelConfig: ThreadListItemLabelConfig = {
order: [
"name",
"participants",
...DEFAULT_THREAD_LIST_ITEM_LABEL_ORDER.slice(1),
],
parts: {
participants: ({ participantCount, t }) =>
participantCount
? t(asDynamicKey("myApp.threadList.participants"), {
count: participantCount,
defaultValue_one: "{{ count }} participant",
defaultValue_other: "{{ count }} participants",
})
: undefined,
},
};
const LabelledThreadListItemUI = (props: ThreadListItemUIProps) => (
<ThreadListItemUI {...props} accessibleLabelConfig={accessibleLabelConfig} />
);
const overrides = { ThreadListItemUI: LabelledThreadListItemUI };
export const WithThreadRowLabels = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);A thread part receives the thread row's data: channel, parentMessage, latestReply, replyCount, participantCount, unreadCount, t and more.
Taking full control
build returns the whole label from the same data, and ignores order, parts and separator:
import { asDynamicKey } from "stream-chat-react";
import type { ChannelListItemLabelConfig } from "stream-chat-react";
const accessibleLabelConfig: ChannelListItemLabelConfig = {
build: ({ displayTitle, t, unreadCount }) => {
const name =
displayTitle ??
t(asDynamicKey("myApp.channelList.unnamed"), "Unnamed channel");
return unreadCount
? t(asDynamicKey("myApp.channelList.nameWithUnread"), {
count: unreadCount,
defaultValue: "{{ name }}, {{ count }} unread",
name,
})
: name;
},
};A row of your own, not built on ChannelListItemUI, can build the same label with composeChannelListItemAccessibleLabel(data, config), or composeThreadListItemAccessibleLabel for a thread. defaultChannelListItemLabelParts and defaultThreadListItemLabelParts hold the default parts, to reuse in your own.
Component props for accessibility
Search inputProps
Search spreads inputProps on its <input>. Use it to keep password managers away from the field, or to add attributes. The input's own props win over yours, its aria-label included, and className is merged:
import { Search } from "stream-chat-react";
// Tells 1Password and LastPass to skip the field.
const passwordManagerOptOut = { "data-1p-ignore": true, "data-lpignore": true };
export const ChatSearch = () => (
<Search inputProps={{ autoComplete: "off", ...passwordManagerOptOut }} />
);Context menu initialFocus
When a menu item opens a submenu with openSubmenu() from useContextMenuContext(), focus moves into the submenu, so it isn't lost when the item that opened it goes away. initialFocus decides where it lands:
| Value | Focus lands on |
|---|---|
'first-item', the default |
the first item, skipping the header and its back button |
'first' |
the first focusable element, the back button included |
'none' |
nowhere: focus doesn't move |
import { ContextMenuButton, useContextMenuContext } from "stream-chat-react";
const LanguageSubmenu = () => (
<>
<ContextMenuButton>English</ContextMenuButton>
<ContextMenuButton>Deutsch</ContextMenuButton>
</>
);
export const LanguageMenuItem = () => {
const { openSubmenu } = useContextMenuContext();
return (
<ContextMenuButton
onClick={() =>
openSubmenu({ initialFocus: "first", Submenu: LanguageSubmenu })
}
>
Language
</ContextMenuButton>
);
};Skip navigation
Skip links let keyboard and screen-reader users jump straight to the main parts of the app. SkipNavigation renders them. Chat doesn't render it: add it yourself, as the first focusable element of your layout.
Basic setup
Give the parts to jump to DOM ids, and pass the ids to SkipNavigation in the order users need them, usually the composer first:
import type { Channel as StreamChannel } from "stream-chat";
import {
Channel,
Chat,
MessageComposer,
MessageList,
SkipNavigation,
} from "stream-chat-react";
const linkLabels: Record<string, string> = {
"message-composer": "Skip to message composer",
"message-list": "Skip to messages",
};
export const App = ({ channel }: { channel: StreamChannel }) => (
<Chat client={client}>
<SkipNavigation
getLinkLabel={(targetId) => linkLabels[targetId] ?? targetId}
targetIds={["message-composer", "message-list"]}
/>
<Channel channel={channel}>
<div id="message-list">
<MessageList />
</div>
<div id="message-composer">
<MessageComposer />
</div>
</Channel>
</Chat>
);Each link is <a href="#message-composer">. Activating it, by click, Enter or Space, moves focus to the element with that id. An element without a tabindex attribute gets tabindex="-1" until it loses focus again, so it can take focus and leaves no extra tab stop. An id may start with #, which is dropped.
Without getLinkLabel, a link reads "Skip to message-composer". That text isn't translated, so pass labels of your own.
| Prop | What it does |
|---|---|
targetIds |
required: the DOM ids to link to, in order |
getLinkLabel |
(targetId, index) => ReactNode: each link's text |
| other props | spread on each <a>, except href: className, onClick, onKeyDown and so on |
An onClick or onKeyDown of yours runs first. Call event.preventDefault() in it to stop the link from moving focus, for example to move it your own way.
Styling the links
Each link has the class str-chat__skip-navigation-link, and a className you pass is added to it. The SDK's stylesheet keeps a link out of view, above the top-left corner of its nearest positioned ancestor, until it has focus. Override the class to place it elsewhere:
.str-chat__skip-navigation-link:focus {
left: 1rem;
top: 1rem;
}View-aware skip links
With ChatView, the parts to jump to differ between the channels view and the threads view. Read the active view with useChatViewContext() and pick the ids:
import { SkipNavigation } from "stream-chat-react";
import { useChatViewContext } from "stream-chat-react/slot-layout";
const targetIdsByView = {
channels: ["message-composer", "channel-list"],
threads: ["thread-composer", "thread-list"],
};
const linkLabels: Record<string, string> = {
"channel-list": "Skip to channel list",
"message-composer": "Skip to message composer",
"thread-composer": "Skip to message composer",
"thread-list": "Skip to thread list",
};
export const ViewAwareSkipNavigation = () => {
const { activeView } = useChatViewContext();
return (
<SkipNavigation
getLinkLabel={(targetId) => linkLabels[targetId] ?? targetId}
targetIds={targetIdsByView[activeView]}
/>
);
};Render it as the first child of ChatView, and give the matching parts of each view those ids. ChatView gives the container of the active view an id of its own, but it's generated for each ChatView and not exported, so use ids you set. With an empty targetIds, SkipNavigation renders nothing.
Localizing announcements
Announcements and accessible names are translated like the rest of the UI. Their keys live in a few namespaces:
a11y.*: the interaction announcements, "New message from Bob", and the shared row parts, such as "Active" and the unread count;channelListItem.*andthreadList.*: most other parts of the row labels;slotLayout.chatView.*: the labels of theChatViewselector;chat.reportLostConnection.*: the lost-connection messages.
Some announcements and row parts reuse shared common.* keys, such as common.newMessages.label for "3 new messages" and common.lastActivity.ariaLabel for a row's time.
The SDK ships English only. To change the English wording, pass the keys to Streami18n, and give it to Chat as i18nInstance:
import type { PropsWithChildren } from "react";
import { Chat, Streami18n } from "stream-chat-react";
const i18nInstance = new Streami18n({
translationsForLanguage: {
"a11y.interactionAnnouncements.openedChannel.ariaLabel":
"Now viewing {{ name }}",
"a11y.accessibleLabel.unreadMessage.ariaLabel_one": "{{ count }} unread",
"a11y.accessibleLabel.unreadMessage.ariaLabel_other": "{{ count }} unread",
},
});
export const App = ({ children }: PropsWithChildren) => (
<Chat client={client} i18nInstance={i18nInstance}>
{children}
</Chat>
);A key that takes a count has a _one and an _other form. For another language, register a dictionary with these keys, as Localization explains.
Some text isn't translated: the severity prefix NotificationAnnouncer puts before a notification, and the default SkipNavigation link text. Replace both, with buildNotificationAnnouncement and getLinkLabel.
Exports reference
Everything is exported from stream-chat-react, except useChatViewContext, which is in stream-chat-react/slot-layout.
| Export | Kind | What it's for |
|---|---|---|
AriaLiveAnnouncerProvider |
component | holds the announcements and announce(); Chat renders it |
AriaLiveOutlet |
component | renders the live regions; add one inside a custom modal dialog |
useAriaLiveAnnouncer |
hook | returns announce(message, options) |
useInteractionAnnouncements |
hook | announces the SDK's interactions by name |
useFocusReturn |
hook | remembers focus before a surface takes it, and gives it back |
useInertWhenHidden |
hook | props that take a mounted, hidden control out of reach |
VisuallyHidden |
component | text for screen readers only |
useIncomingMessageAnnouncements |
hook | announces new messages in a channel or thread |
NotificationAnnouncer |
component | reads notifications aloud; Chat renders it |
SkipNavigation |
component | skip links |
composeChannelListItemAccessibleLabel |
function | builds a channel row's accessible name |
composeThreadListItemAccessibleLabel |
function | builds a thread row's accessible name |
defaultChannelListItemLabelParts |
object | the default channel row parts |
defaultThreadListItemLabelParts |
object | the default thread row parts |
DEFAULT_CHANNEL_LIST_ITEM_LABEL_ORDER |
array | the default channel row order |
DEFAULT_THREAD_LIST_ITEM_LABEL_ORDER |
array | the default thread row order |
ChannelListItemLabelConfig |
type | the accessibleLabelConfig of ChannelListItemUI |
ThreadListItemLabelConfig |
type | the accessibleLabelConfig of ThreadListItemUI |
ComponentContext lists the NotificationAnnouncer, ChannelListItemUI and ThreadListItemUI override keys.