Channel UI
An open channel is a panel with stacked components, such as a header, a message list and a composer. <Channel/> provides one channel to every component inside it, and you choose which components to render. Use this page to set up a channel view, change its header and show who is typing. Channel details are on Channel details UI. The props are in the Channel and ChannelHeader references.

This is the Design review channel. Its three parts, from top to bottom:
| Part | Component | Explained in |
|---|---|---|
| 1. The header | <ChannelHeader/> |
The header |
| 2. The message list | <MessageList/> |
Message list UI |
| 3. The message composer | <MessageComposer/> |
Message composer UI |
The Channel component
<Channel channel={channel}> binds a channel to its children:
<Channel/> |
What it means for you |
|---|---|
| provides the channel to the components inside | They retrieve it with useChannel(). <MessageList/> renders channel.messagePaginator, and <MessageComposer/> writes into channel.messageComposer. |
| activates the channel while it's mounted | The client knows the channel is in use. Entity lifecycle explains what that keeps in memory. |
renders the channel's container, .str-chat__channel, around the children |
Style the channel column through that class. |
| provides its own audio players, for voice recordings and audio attachments | When <Channel/> unmounts or gets another channel, they stop, so audio from the channel the user left doesn't keep playing. |
<Channel/> doesn't load the channel. Pass a channel that's already watched, for example with Channel.watch(), as in Show one channel. With <ChatView/>, <ChannelSlot/> renders <Channel/> for the channel in its slot.
<ChannelPlaceholder/> renders the same container with no channel, for the space a channel fills while none is selected. Here is a ChannelPanel component that renders a channel, or the placeholder while none is selected:
import type { Channel as StreamChannel } from "stream-chat";
import {
Channel,
ChannelHeader,
ChannelPlaceholder,
MessageComposer,
MessageList,
} from "stream-chat-react";
export const ChannelPanel = ({ channel }: { channel?: StreamChannel }) => {
if (!channel) {
return (
<ChannelPlaceholder>Pick a channel to start chatting.</ChannelPlaceholder>
);
}
return (
<Channel channel={channel}>
<ChannelHeader />
<MessageList />
<MessageComposer />
</Channel>
);
};Providers and context hooks explains the contexts <Channel/> provides.
The header
<ChannelHeader/> shows the channel it gets from useChannel():

This is the header of a channel opened over another one in a panel that can be closed, so both its back and close buttons show. It has five areas, numbered from start to end. The table lists what each shows by default, and the <ChannelHeader/> prop that replaces it:
| Area | Shows by default | Prop that replaces it |
|---|---|---|
| 1. Start | The back button, <WorkspaceNavigationBackButton/>. It shows only when the channel is stacked over other content in its panel, and goes back to what's beneath. |
StartContent |
| 2. Title | The channel's name, from channel.state: data.name. A channel without a name shows the other member's name in a direct message, and two other members' names in a group. |
title replaces the text, Title the component that renders it |
| 3. Subtitle | Who is typing, from channel.state: typing. Otherwise "Online" or "Offline" in a direct message, and "12 members · 3 online" in a group, from channel.state: memberCount and watcherCount. |
Subtitle |
| 4. Avatar | The channel image, or its members' avatars, drawn by <ChannelAvatar/>. It doesn't open channel details by default; see Channel details UI. |
image replaces the image, Avatar the whole avatar component |
| 5. End | The close button, <WorkspaceNavigationCloseButton/>. It shows only when the channel's panel can be closed, which the main channel's panel can't. |
EndContent |
Both buttons come from the layout. Without one, neither renders. Moving between channels, threads and messages explains back and close.
A component passed as StartContent or EndContent replaces the button. To add something and keep the button, render the button yourself. Here is a ChannelHeaderWithCall component that adds a Call button before the close button:
import {
ChannelHeader,
useChannel,
WorkspaceNavigationCloseButton,
} from "stream-chat-react";
const HeaderEnd = () => {
const channel = useChannel();
return (
<>
<button
onClick={() => console.log("Start a call in", channel.cid)}
type="button"
>
Call
</button>
<WorkspaceNavigationCloseButton />
</>
);
};
export const ChannelHeaderWithCall = () => (
<ChannelHeader EndContent={HeaderEnd} />
);Title and Subtitle replace the title and the subtitle and keep the rest of the header. Title receives the channel and displayTitle, the title the header would show, including a title you pass. Here is a ChannelHeaderWithTitleButton component whose title is a button:
import type { ChannelHeaderTitleProps } from "stream-chat-react";
import { ChannelHeader } from "stream-chat-react";
const TitleButton = ({ channel, displayTitle }: ChannelHeaderTitleProps) => (
<button
className="str-chat__channel-header__data__title"
onClick={() => console.log("Show details of", channel.cid)}
type="button"
>
{displayTitle}
</button>
);
export const ChannelHeaderWithTitleButton = () => (
<ChannelHeader Title={TitleButton} />
);The str-chat__channel-header__data__title class keeps the title's look. The Channel details recipe uses such a title to open channel details.
A Subtitle component replaces the typing indicator too. To keep it, render <TypingIndicatorHeader/> in your subtitle, or render the default <ChannelHeaderSubtitle/> inside it. <TypingIndicatorHeader/> renders nothing while nobody is typing, so your subtitle shows its own text next to it. useChannelHeaderOnlineStatus() returns the default subtitle's text: "Online" or "Offline" in a direct message, and "12 members · 3 online" in a group.
Typing indicator
Two components show who is typing:
| Component | Where | Shows |
|---|---|---|
<TypingIndicator/> |
at the bottom of <MessageList/>; <VirtualizedMessageList/> renders it only when you register a TypingIndicator override |
the avatars of the people typing and animated dots, only while the list is scrolled to the latest message. It also announces who is typing to screen readers. |
<TypingIndicatorHeader/> |
in place of the header's subtitle | the "Cleo is typing" text |
Both read channel.state.typing, which holds the latest typing event of each user in the channel. A typing event from a thread carries the thread's id, so the indicators can tell typing in the channel from typing in a thread. They leave out the current user, and keep showing for two seconds after typing stops, so they don't flicker.
What they show depends on where they're rendered:
| Rendered inside | <TypingIndicator/> shows |
<TypingIndicatorHeader/> shows |
|---|---|---|
a <Channel/> |
the people typing a message in the channel. People typing a reply in one of its threads aren't shown. | the same as <TypingIndicator/> |
a <Thread/> |
the people typing a reply in that thread | by default, the people typing in the channel, like in a <Channel/>. Pass it threadList to show the people typing a reply in that thread instead. |
Example
Bob is typing a reply in the thread of the user's message "Lunch?", and Cleo is typing a new message in the channel:
- The channel's message list shows that Cleo is typing. Bob isn't shown there.
- The thread's message list shows that Bob is typing.
- A header with
<TypingIndicatorHeader/>shows Cleo, unless it hasthreadListand is inside the thread: then it shows Bob.
They render nothing when typing events are turned off, with typingEvents.enabled under channel in client.config.
Typing indicators covers the feature end to end: sending typing events and building your own indicator.
Replacing parts
<Channel/> renders only its children, so you replace a part of the channel by rendering a different component in its place. A header of your own can retrieve the channel with useChannel(); see the Channel header recipe.
<ChannelHeader/> has no <WithComponents/> key. Change it through its props:
| Prop | Replaces |
|---|---|
StartContent |
The back button |
EndContent |
The close button |
Avatar |
The avatar component |
Title |
The component that renders the title |
Subtitle |
The subtitle, including the typing indicator |
title |
The title's text, a string |
image |
The avatar image |
The typing indicator in the message lists is the one part on this page you replace through <WithComponents/>, with the TypingIndicator key.
The message list and the composer have keys of their own, listed on Message list UI and Message composer UI. Overriding components explains how overrides apply.