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>
);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:
partsis merged over the defaults, so a new key is added and a same-named key overrides.orderdecides which parts render and in what sequence. A part not listed inorderis never rendered, even if defined.separatorjoins non-empty parts (defaults to'. ').buildbypasses 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.
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.