Accessible List Labels

Every channel and thread list row exposes a single, composed accessible name so a screen reader announces one coherent sentence per row instead of walking each nested element. This recipe shows how to reshape that name: adding your own segments, reordering them, and taking full control.

For the underlying model and API surface see the Accessibility guide.

How the label is built

The name is assembled from named parts, joined by a separator, in a defined order. ChannelListItemUI and ThreadListItemUI accept an accessibleLabelConfig prop that lets you override any of this:

  • parts is merged over the defaults, so a new key is added and a same-named key overrides.
  • order decides which parts render and in what sequence. A part not listed in order is never rendered, even if defined.
  • separator joins non-empty parts (defaults to '. ').
  • build bypasses everything and returns the whole string yourself.

The default channel row reading order is name, active, unread count, last message, attachments, link preview, delivery status, time. The default thread row order is channel name, active, unread count, parent message, reply count, last activity.

Adding custom parts to a channel row

Supply your own ChannelListItemUI through ComponentContext and forward an accessibleLabelConfig. Below we announce Pinned, Muted, and Frozen state right after the channel name. Each part returns its segment string, or undefined to be omitted, so Pinned is only spoken when the channel is actually pinned.

import {
  Channel,
  ChannelList,
  ChannelListItemUI as DefaultChannelListItemUI,
  Chat,
  DEFAULT_CHANNEL_LIST_ITEM_LABEL_ORDER,
  type ChannelListItemLabelConfig,
  type ChannelListItemUIProps,
} from "stream-chat-react";

const accessibleLabelConfig: ChannelListItemLabelConfig = {
  parts: {
    pinned: ({ channel, t }) =>
      channel.state.membership?.pinned_at ? t("Pinned") : undefined,
    muted: ({ channel, t }) =>
      channel.muteStatus().muted ? t("Muted") : undefined,
    frozen: ({ channel, t }) =>
      channel.data?.frozen ? t("Frozen") : undefined,
  },
  // Spread the defaults so you keep the built-in segments, then insert your own after `name`.
  order: [
    "name",
    "pinned",
    "muted",
    "frozen",
    ...DEFAULT_CHANNEL_LIST_ITEM_LABEL_ORDER.slice(1),
  ],
};

const CustomChannelPreview = (props: ChannelListItemUIProps) => (
  <DefaultChannelListItemUI
    {...props}
    accessibleLabelConfig={accessibleLabelConfig}
  />
);

export const App = ({ client }) => (
  <Chat client={client}>
    <ChannelList
      filters={{ type: "messaging", members: { $in: [client.userID] } }}
      Preview={CustomChannelPreview}
    />
    <Channel>{/* ... */}</Channel>
  </Chat>
);

A pinned, muted channel now reads roughly:

General. Pinned. Muted. 3 unread messages. Last message from Alice: see you soon. 2 days ago

Parts call the translate function t, so provide your own i18n keys (or return plain strings). The SDK does not ship Pinned, Muted, or Frozen strings.

Reordering or dropping default parts

Because order is the single source of truth for what renders, you can trim a verbose label by listing only the parts you want. This announces just the name, unread count, and last message, dropping attachments, link previews, delivery status, and time:

const accessibleLabelConfig: ChannelListItemLabelConfig = {
  order: ["name", "unreadCount", "lastMessage"],
};

Customizing thread rows

ThreadListItemUI works the same way with ThreadListItemLabelConfig and DEFAULT_THREAD_LIST_ITEM_LABEL_ORDER. Here we add the participant count after the channel name:

import {
  ThreadListItemUI as DefaultThreadListItemUI,
  DEFAULT_THREAD_LIST_ITEM_LABEL_ORDER,
  type ThreadListItemLabelConfig,
  type ThreadListItemUIProps,
} from "stream-chat-react";

const accessibleLabelConfig: ThreadListItemLabelConfig = {
  parts: {
    participants: ({ participantCount, t }) =>
      participantCount
        ? t("{{ count }} participants", { count: participantCount })
        : undefined,
  },
  order: [
    "name",
    "participants",
    ...DEFAULT_THREAD_LIST_ITEM_LABEL_ORDER.slice(1),
  ],
};

const CustomThreadListItem = (props: ThreadListItemUIProps) => (
  <DefaultThreadListItemUI
    {...props}
    accessibleLabelConfig={accessibleLabelConfig}
  />
);
// Provide { ThreadListItemUI: CustomThreadListItem } through ComponentContext.

Taking full control

When part composition is not enough, set build to return the entire string. It receives the same row data and ignores order, parts, and separator:

const accessibleLabelConfig: ChannelListItemLabelConfig = {
  build: ({ channel, displayTitle, unreadCount, t }) => {
    const name = displayTitle ?? channel.data?.name ?? t("Unnamed channel");
    return unreadCount
      ? t("{{ name }}, {{ count }} unread", { count: unreadCount, name })
      : name;
  },
};

Alternatively, write a fully custom ChannelListItemUI and call composeChannelListItemAccessibleLabel directly, importing defaultChannelListItemLabelParts to reuse the built-in segments.