# Channel header

This recipe builds a channel header of its own. The default header and its props are explained in [Channel UI](https://getstream.io/chat/docs/sdk/react/ui/components/channel-ui/#the-header). To change only the title or the subtitle, you don't need a header of your own: pass a component as the header's `Title` or `Subtitle`, as that section shows.

>
> **Note:** **What you'll build**
>
> - A channel header with the channel's avatar and title
> - Member and online counts in the subtitle
> - Who is typing in the subtitle
>

---

**Preview**

![A custom channel header with the channel avatar, its title, and the member and online counts](https://getstream.io/docs-assets/images/db0614a250fb.png)

---

**Approach:** Compose. [Choosing where to start](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/overview/#choosing-where-to-start) explains the approaches.

**Starts from:** [the base app](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/overview/#the-base-app). The page shows only what the recipe adds.

`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 "Bob 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 label="React" 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="cookbook-channel-header">
      <WorkspaceNavigationBackButton />
      <ChannelAvatar
        displayMembers={groupChannelDisplayInfo.members}
        imageUrl={displayImage}
        size="lg"
        userName={displayTitle}
      />
      <div className="cookbook-channel-header__content">
        <div className="cookbook-channel-header__title">{displayTitle}</div>
        <div className="cookbook-channel-header__subtitle">
          {typingEventsEnabled && someoneIsTyping ? (
            <TypingIndicatorHeader />
          ) : (
            `${memberCount} members · ${watcherCount} online`
          )}
        </div>
      </div>
      <WorkspaceNavigationCloseButton />
    </div>
  );
};
```

```css label="CSS" title="src/CustomChannelHeader.css"
@layer stream-overrides {
  .cookbook-channel-header {
    display: flex;
    align-items: center;
    gap: var(--str-chat__spacing-sm);
    padding: var(--str-chat__spacing-sm) var(--str-chat__spacing-md);
    border-bottom: 1px solid var(--str-chat__border-core-default);
    background: var(--str-chat__background-core-elevation-1);
  }

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

  .cookbook-channel-header__title {
    color: var(--str-chat__text-primary);
    font: var(--str-chat__font-heading-xs);
  }

  .cookbook-channel-header__subtitle {
    color: var(--str-chat__text-secondary);
    font: var(--str-chat__font-caption-default);
  }
}
```

The rules sit in the `stream-overrides` layer, so they win over the SDK's own rules once the SDK's stylesheet is in an earlier layer, as [Theming → CSS layers](https://getstream.io/chat/docs/sdk/react/features/app-experience/theming/#css-layers) sets up. They take their colors, spacing and type from the SDK's tokens, so they follow your theme.

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/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 "Bob is typing", "Bob and Cara are typing" or "3 people are typing" from the same events. [Typing indicators](https://getstream.io/chat/docs/sdk/react/features/messaging/typing-indicators/) covers typing end to end.

---

**While a member types**

![The same header while another member types: the subtitle reads "Bob is typing"](https://getstream.io/docs-assets/images/f8b529994f35.png)

---

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

## 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>
);
```

Without a layout, render the header inside `<Channel channel={channel}>` the same way.

## How it works

>
> **Info:** **Example**
>
> The user has Design review open, and Bob starts typing:
>
> 1. `CustomChannelHeader` shows the channel's title and image from `useChannelPreviewInfo()`, and the subtitle from `channel.state.memberCount` and `channel.state.watcherCount`, such as "4 members · 2 online".
> 2. Bob starts typing. His typing event arrives, `channel.state.typing` gets an entry for him, and `useStateStore` renders the header again.
> 3. The event has no `parent_id` and isn't the user's own, so `someoneIsTyping` is `true`. The subtitle shows `TypingIndicatorHeader`: "Bob is typing".
> 4. Bob stops typing. His entry leaves `channel.state.typing`, and the subtitle shows the counts again.
>

A reply Bob types in a thread carries the thread's id as `parent_id`, so it doesn't change this header. When `channel.configState` says typing events are off, the subtitle always shows the counts.

## Related

- [Channel UI](https://getstream.io/chat/docs/sdk/react/ui/components/channel-ui/#the-header): the default header and its props.
- [What channels and threads hold](https://getstream.io/chat/docs/sdk/react/state/channels-and-threads/what-channels-and-threads-hold/): the counts and typing events in `channel.state`.
- [Typing indicators](https://getstream.io/chat/docs/sdk/react/features/messaging/typing-indicators/): typing end to end.
- [Reading state in components](https://getstream.io/chat/docs/sdk/react/ui/reading-state-in-components/): `useStateStore` and its selectors.
- [Back and close](https://getstream.io/chat/docs/sdk/react/ui/layout/moving-between-channels-threads-and-messages/#back-and-close): what the header's back and close buttons do.
- [Thread header](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/headers/thread-header/): the same approach for the thread's header.

---

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