# Channel header

> **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/).

This recipe builds a channel header of its own: the avatar on the left, then the title, and a subtitle with who is typing, or the member and online counts. The default header and its props are explained in [Channel UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/channel-ui/#the-header).

`ChannelHeader` has no override key. `Channel` renders only its children, so a header of your own goes where `ChannelHeader` would.

## Best practices

- Read the title, image and group avatar from `useChannelPreviewInfo()`. It follows changes to the channel's name, image and members.
- Read counts and typing from `channel.state` with `useStateStore`, and declare the selectors at module scope.
- Show typing with `TypingIndicatorHeader`, the header's "Cleo is typing" text, not the message list's `TypingIndicator`.
- Hide the typing text when typing events are off for the channel, in its configuration (`channel.configState`).
- Keep the back and close buttons, `WorkspaceNavigationBackButton` and `WorkspaceNavigationCloseButton`. They render only when the layout allows going back or closing the panel.
- Keep the header light: it re-renders on every typing event.

## Implementation

The default header switches its subtitle between typing and the online status. This one does the same:

```tsx title="src/CustomChannelHeader.tsx"
import type {
  ChannelConfig,
  ChannelStateData,
  EventPayload,
} from "stream-chat";
import {
  ChannelAvatar,
  TypingIndicatorHeader,
  useChannel,
  useChannelPreviewInfo,
  useChatContext,
  useStateStore,
  WorkspaceNavigationBackButton,
  WorkspaceNavigationCloseButton,
} from "stream-chat-react";
import "./CustomChannelHeader.css";

const channelStateSelector = ({
  memberCount,
  typing,
  watcherCount,
}: ChannelStateData) => ({ memberCount, typing, watcherCount });

const typingEventsSelector = ({ typingEvents }: ChannelConfig) => ({
  typingEventsEnabled: typingEvents.enabled,
});

export const CustomChannelHeader = () => {
  const { client } = useChatContext();
  const channel = useChannel();
  const { memberCount, typing, watcherCount } = useStateStore(
    channel.state,
    channelStateSelector,
  );
  const { typingEventsEnabled } = useStateStore(
    channel.configState,
    typingEventsSelector,
  );
  const { displayImage, displayTitle, groupChannelDisplayInfo } =
    useChannelPreviewInfo({ channel });

  // Others typing in the channel itself; replies typed in threads carry a parent_id.
  const someoneIsTyping = (
    Object.values(typing) as EventPayload<"typing.start">[]
  ).some(({ parent_id, user }) => !parent_id && user?.id !== client.userID);

  return (
    <div className="custom-channel-header">
      <WorkspaceNavigationBackButton />
      <ChannelAvatar
        displayMembers={groupChannelDisplayInfo.members}
        imageUrl={displayImage}
        size="lg"
        userName={displayTitle}
      />
      <div className="custom-channel-header__content">
        <div className="custom-channel-header__title">{displayTitle}</div>
        <div className="custom-channel-header__subtitle">
          {typingEventsEnabled && someoneIsTyping ? (
            <TypingIndicatorHeader />
          ) : (
            `${memberCount} members · ${watcherCount} online`
          )}
        </div>
      </div>
      <WorkspaceNavigationCloseButton />
    </div>
  );
};
```

```css title="src/CustomChannelHeader.css"
.custom-channel-header {
  align-items: center;
  display: flex;
  gap: 12px;
  padding: 12px 16px;
}

.custom-channel-header__content {
  flex: 1;
  min-width: 0;
}

.custom-channel-header__title {
  font-weight: 600;
}

.custom-channel-header__subtitle {
  color: #6b7280;
  font-size: 0.875rem;
}
```

Here is what each part reads:

- `useChannelPreviewInfo({ channel })` gives the title and image the channel list shows: the channel's name, or its members' names when it has none. `groupChannelDisplayInfo.members` are the members `ChannelAvatar` draws when the channel has no image. It works out the overflow badge itself.
- `channel.state` holds `memberCount`, `watcherCount`, the number of users watching the channel right now, and `typing`, the latest typing event of each user. [What channels and threads hold](https://getstream.io/chat/docs/sdk/react/v15/state/channels-and-threads/what-channels-and-threads-hold/) explains the rest.
- `channel.configState` holds the channel's resolved configuration. `typingEvents.enabled` is `false` when the client configuration or the channel type turns typing events off.
- `TypingIndicatorHeader` renders "Cleo is typing", "Cleo and Bob are typing" or "3 people are typing" from the same events. [Typing indicators](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/typing-indicators/) covers typing end to end.

To open channel details from the avatar, render `AvatarWithChannelDetail` from `stream-chat-react/channel-detail` in place of `ChannelAvatar`. See [Channel details](https://getstream.io/chat/docs/sdk/react/v15/ui/components/channel-ui/#channel-details).

## Final code

Render the header where `ChannelHeader` would go. With `ChatView`, that's inside `ChannelSlot`:

```tsx title="src/App.tsx"
import {
  ChannelNavigation,
  Chat,
  MessageComposer,
  MessageList,
  ThreadHeader,
} from "stream-chat-react";
import {
  ChannelSlot,
  ChatView,
  ThreadSlot,
} from "stream-chat-react/slot-layout";
import type { LayoutDescriptor } from "stream-chat-react/slot-layout";
import { CustomChannelHeader } from "./CustomChannelHeader";

const layouts: LayoutDescriptor[] = [
  { id: "channels", slots: ["channel", "thread"] },
];

const ChannelsView = () => (
  <>
    <ChannelNavigation />
    <ChannelSlot slot="channel">
      <CustomChannelHeader />
      <MessageList />
      <MessageComposer />
    </ChannelSlot>
    <ThreadSlot slot="thread">
      <ThreadHeader />
      <MessageList />
      <MessageComposer />
    </ThreadSlot>
  </>
);

export const App = () => (
  <Chat client={client}>
    <ChatView layouts={layouts} views={{ channels: <ChannelsView /> }} />
  </Chat>
);
```

The channel list comes from the paginators registered on `client.channelManager`, as in the [Quick start](https://getstream.io/chat/docs/sdk/react/v15/getting-started/quick-start/#add-a-channel-list-with-chatview). Without a layout, render the header inside `<Channel channel={channel}>` the same way.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/recipes/headers/channel-header/](https://getstream.io/chat/docs/sdk/react/v15/recipes/headers/channel-header/).