# Accessibility

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

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`](https://getstream.io/chat/docs/sdk/react/v15/component-reference/chat/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 `Home` and `End` jump 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 marked `aria-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:

```tsx
// 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()` returns `announce(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:

```tsx
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:

```tsx
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](#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:

```tsx
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:

- `debounceMs` waits 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.
- `delayMs` waits 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:

```tsx
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:

```tsx
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](#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`:

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

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

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

>
> **Note:** 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:

```tsx
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](https://getstream.io/chat/docs/sdk/react/v15/features/app-experience/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](#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:

- `parts` is merged over the default parts: a new name adds a part, an existing name replaces it.
- `order` decides which parts are read, and in what order. A part not in `order` is never read, even if it's defined.
- `separator` joins the parts that aren't empty. It defaults to `". "`.
- `build` returns 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:

```tsx
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](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/localization/#your-own-keys-and-loosetranslationdictionary) 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:

```tsx
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:

```tsx
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`:

```tsx
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:

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

```tsx
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:

```tsx
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:

```css
.str-chat__skip-navigation-link:focus {
  left: 1rem;
  top: 1rem;
}
```

### View-aware skip links

With [`ChatView`](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-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:

```tsx
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.*` and `threadList.*`: most other parts of the row labels;
- `slotLayout.chatView.*`: the labels of the `ChatView` selector;
- `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`:

```tsx
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](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/localization/#registering-dictionaries) 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](https://getstream.io/chat/docs/sdk/react/v15/component-reference/channel/componentcontext/) lists the `NotificationAnnouncer`, `ChannelListItemUI` and `ThreadListItemUI` override keys.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/features/app-experience/accessibility/](https://getstream.io/chat/docs/sdk/react/v15/features/app-experience/accessibility/).