Channel UI
An open channel is a column of parts: a header, a message list and a composer. Channel gives one channel to every part inside it, and the parts are yours to choose. This page covers Channel itself, the channel header, the typing indicator and channel details. The props are in the Channel and ChannelHeader references.
The Channel component
<Channel channel={channel}> binds a channel to its children:
- It gives the channel to the components inside. They read it with
useChannel().MessageListrenderschannel.messagePaginator, andMessageComposerwrites intochannel.messageComposer. - It activates the channel while it is mounted, which tells the client the channel is in use. Entity lifecycle explains what that keeps in memory.
- It refreshes the channel's unread snapshot, which the unread separator and the unread notification render from. A channel that was open before isn't queried again, so this is what keeps them right when the user comes back to it.
- It renders the channel's container,
.str-chat__channel, around the children. - It gives the channel its own audio players, for voice recordings and audio attachments. When
Channelunmounts or itschannelchanges, it stops them, so audio from the channel the user left doesn't keep playing.
Channel doesn't load the channel. Pass a channel that is 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:
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>
);
};How components connect to state explains the contexts Channel provides.
The header
ChannelHeader shows the channel from useChannel(). From start to end:
StartContent, by default the back button,WorkspaceNavigationBackButton. It shows only when the channel is stacked over other content in its panel, and goes back to what is beneath.- The title: the channel's name. A channel without one shows the other member's name in a direct message, and two other members' names in a group. The
titleprop replaces it. - The subtitle: who is typing. Otherwise "Online" or "Offline" in a direct message, and "12 members · 3 online" in a group, where online counts the channel's watchers.
- The avatar: the channel image, or its members' avatars, drawn by
ChannelAvatar. Theimageprop replaces the image, and theAvatarprop replaces the component. The default avatar doesn't open channel details; see Channel details. EndContent, by default the close button,WorkspaceNavigationCloseButton. It shows only when the channel's panel can be closed, which the main channel's panel can't.

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:
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} />
);Typing indicator
Two components show who is typing:
TypingIndicatorrenders at the bottom ofMessageList: the avatars of the people typing and animated dots. It shows only while the list is scrolled to the latest message, and announces who is typing to screen readers.VirtualizedMessageListrenders it only when you register aTypingIndicatoroverride.TypingIndicatorHeaderis the "Cleo is typing" text that replaces the header's subtitle.
Both read the same data. The channel keeps the latest typing event of each user in channel.state.typing and copies it into its composer, channel.messageComposer.textComposer.state.typing. The indicators read it from the composer that useMessageComposerController() returns. They leave out the current user and replies typed in threads, and keep showing for two seconds after typing stops, so they don't flicker. Inside a Thread they read the thread's composer, which receives no typing events, so they show nothing there. They render nothing when typing events are turned off for the channel (typingEvents.enabled in its configuration).
Typing indicators covers the feature end to end: sending typing events and building your own indicator.
Channel details
Channel details is a panel with the channel's info, members, pinned messages, photos and videos, and files. It lives in its own entry point, stream-chat-react/channel-detail, with its own stylesheet.
The entry is the avatar, once you wire it in: ChannelHeader doesn't do it by default. AvatarWithChannelDetail renders the channel avatar as a button, and clicking it opens ChannelDetail in a modal. Pass it to the header as its avatar:
import {
Channel,
ChannelHeader,
MessageComposer,
MessageList,
} from "stream-chat-react";
import { AvatarWithChannelDetail } from "stream-chat-react/channel-detail";
import "stream-chat-react/dist/css/index.css";
import "stream-chat-react/dist/css/channel-detail.css";
export const ChannelWithDetails = () => (
<Channel channel={channel}>
<ChannelHeader Avatar={AvatarWithChannelDetail} />
<MessageList />
<MessageComposer />
</Channel>
);The button is disabled until the channel exists on the server, because the details are loaded from there. The modal is the Modal override, by default the SDK's GlobalModal.
ChannelDetail is a section navigator with five sections, in this order: channel info, members, pinned messages, photos and videos, and files. Its sections prop chooses and orders them; defaultChannelDetailSections is the default list. The navigator picks its layout from the width it has:
- Tabs, at 640px and wider: the list of sections beside the open section.
- Inline, when narrower: one section at a time, with a menu button in its header that opens the list of sections.
A view inside a section, such as one member's details, opens on top of the section and shows a back button.
To show details in a panel of your own instead of a modal, render <ChannelDetail channel={channel} /> there. The actions in channel details are action sets, covered in Actions. The props are in the Channel Detail reference.
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 read the channel with useChannel(); see the Channel header recipe.
ChannelHeader has no override key. Change it through its props:
| Prop | Replaces |
|---|---|
StartContent |
The back button |
EndContent |
The close button |
Avatar |
The avatar component |
title |
The title |
image |
The avatar image |
The parts on this page that render from an override key:
| Override key | Replaces |
|---|---|
TypingIndicator |
The typing indicator in the message lists |
Modal |
The modal that channel details open in, and other modals in the SDK |
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.