# Thread header

This recipe builds a thread header of its own. The default header is explained in [Thread UI](https://getstream.io/chat/docs/sdk/react/ui/components/thread-ui/#the-thread-header).

>
> **Note:** **What you'll build**
>
> - A thread header with the participants' avatars
> - The channel name and reply count in the subtitle
> - Who is typing a reply in the subtitle
>

---

**Preview**

![A custom thread header with the avatars of the participants, the title Thread, the channel name and the reply count](https://getstream.io/docs-assets/images/f2475b5a7f13.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.

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

## Best practices

- Read the thread from `useThreadContext()` and subscribe to `thread.state` for the reply count and the participants. Declare the selectors at module scope.
- Take the channel's name from `useChannelPreviewInfo()`, as the default header does.
- Keep the back and close buttons, `WorkspaceNavigationBackButton` and `WorkspaceNavigationCloseButton`. They render only when the layout allows going back or closing the thread's panel. A button of your own closes the thread with `useCloseThread()`.
- Show typing with `<TypingIndicatorHeader threadList />`. With `threadList`, it shows the users typing a reply in this thread, not in the channel.
- Hide the typing text when typing events are off for the channel, in its configuration (`channel.configState`).
- Keep the header compact, so the replies keep the space.

## Create the component

```tsx label="React" title="src/CustomThreadHeader.tsx"
import type {
  ChannelConfig,
  ChannelStateData,
  EventPayload,
  ThreadState,
} from "stream-chat";
import {
  Avatar,
  TypingIndicatorHeader,
  useChannel,
  useChannelPreviewInfo,
  useChatContext,
  useStateStore,
  useThreadContext,
  WorkspaceNavigationBackButton,
  WorkspaceNavigationCloseButton,
} from "stream-chat-react";
import "./CustomThreadHeader.css";

const threadStateSelector = ({ participants, replyCount }: ThreadState) => ({
  participants,
  replyCount,
});
const typingSelector = ({ typing }: ChannelStateData) => ({ typing });
const typingEventsSelector = ({ typingEvents }: ChannelConfig) => ({
  typingEventsEnabled: typingEvents.enabled,
});

export const CustomThreadHeader = () => {
  const { client } = useChatContext();
  const channel = useChannel();
  const thread = useThreadContext();
  const { participants = [], replyCount = 0 } =
    useStateStore(thread?.state, threadStateSelector) ?? {};
  const { typing } = useStateStore(channel.state, typingSelector);
  const { typingEventsEnabled } = useStateStore(
    channel.configState,
    typingEventsSelector,
  );
  const { displayTitle } = useChannelPreviewInfo({ channel });

  // Others typing a reply in this thread. Typing events carry the thread's id as parent_id.
  const someoneIsTyping = (
    Object.values(typing) as EventPayload<"typing.start">[]
  ).some(
    ({ parent_id, user }) =>
      parent_id === thread?.id && user?.id !== client.userId,
  );

  return (
    <div className="cookbook-thread-header">
      <WorkspaceNavigationBackButton />
      <div className="cookbook-thread-header__participants">
        {participants.map(({ user }) =>
          user ? (
            <Avatar
              imageUrl={user.image}
              key={user.id}
              size="sm"
              userName={user.name ?? user.id}
            />
          ) : null,
        )}
      </div>
      <div className="cookbook-thread-header__content">
        <div className="cookbook-thread-header__title">Thread</div>
        <div className="cookbook-thread-header__subtitle">
          {typingEventsEnabled && someoneIsTyping ? (
            <TypingIndicatorHeader threadList />
          ) : (
            `${displayTitle} · ${replyCount} ${replyCount === 1 ? "reply" : "replies"}`
          )}
        </div>
      </div>
      <WorkspaceNavigationCloseButton />
    </div>
  );
};
```

```css label="CSS" title="src/CustomThreadHeader.css"
@layer stream-overrides {
  .cookbook-thread-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-thread-header__participants {
    display: flex;
    gap: var(--str-chat__spacing-xxs);
  }

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

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

  .cookbook-thread-header__subtitle {
    overflow: hidden;
    color: var(--str-chat__text-secondary);
    font: var(--str-chat__font-caption-default);
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}
```

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:

- `useThreadContext()` returns the thread that `Thread` or `ThreadSlot` renders. `thread.state` holds the participants, each with a `user`, and the reply count. [What channels and threads hold](https://getstream.io/chat/docs/sdk/react/state/channels-and-threads/what-channels-and-threads-hold/) explains the rest.
- `useChannel()` returns the thread's channel, so `useChannelPreviewInfo()` gives its name.
- `channel.state.typing` holds the latest typing event of each user in the channel. An event sent from a thread carries the thread's id as `parent_id`, so the header filters on it to decide whether to show the typing text. The filter is the one from [Typing in threads](https://getstream.io/chat/docs/sdk/react/features/messaging/typing-indicators/#typing-in-threads).
- `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` with `threadList` renders "Bob is typing", "Bob and Cara are typing" or "3 people are typing" from the same events, translated like the rest of the SDK.

The reply count text here is plain English. To translate it, use `t` from `useTranslationContext()`, as [Localization](https://getstream.io/chat/docs/sdk/react/features/app-experience/localization/) explains.

## Render it in the thread

Put the header where `ThreadHeader` would go. With `ChatView`, that's inside `ThreadSlot`:

```tsx title="src/ThreadPanel.tsx"
import {
  MessageComposer,
  MessageList,
  useThreadContext,
} from "stream-chat-react";
import { ThreadSlot, useActiveThread } from "stream-chat-react/slot-layout";
import { CustomThreadHeader } from "./CustomThreadHeader";

// Marks the thread as on screen, so its new replies are marked read.
const ActiveThread = () => {
  const thread = useThreadContext();
  useActiveThread({ activeThread: thread });
  return null;
};

export const ThreadPanel = () => (
  <ThreadSlot slot="thread">
    <ActiveThread />
    <CustomThreadHeader />
    <MessageList />
    <MessageComposer />
  </ThreadSlot>
);
```

`ActiveThread` tells the thread it's on screen, so its new replies are marked read, as in [Channel and thread side by side](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/channel-and-thread-side-by-side/). Render `ThreadPanel` in place of the base app's `ThreadSlot`. Without a layout, render the header inside `<Thread thread={thread}>` the same way.

## How it works

>
> **Info:** **Example**
>
> The user opens the thread under Bob's message in Design review, and Cara starts typing a reply:
>
> 1. `ThreadSlot` renders the thread, and `CustomThreadHeader` reads it with `useThreadContext()`. The header shows the participants' avatars from `thread.state.participants`, and the subtitle from the channel's name and `thread.state.replyCount`, such as "Design review · 3 replies".
> 2. Cara starts typing in the thread. Her typing event carries the thread's id as `parent_id`, and lands in `channel.state.typing`.
> 3. `someoneIsTyping` is `true`, so the subtitle shows `<TypingIndicatorHeader threadList />`: "Cara is typing".
> 4. Cara sends her reply. Her typing entry goes, `thread.state.replyCount` goes up, and the subtitle reads "Design review · 4 replies".
>

Typing in the channel itself carries no `parent_id`, so it doesn't change this header.

## Related

- [Thread UI](https://getstream.io/chat/docs/sdk/react/ui/components/thread-ui/#the-thread-header): the default thread header.
- [What channels and threads hold](https://getstream.io/chat/docs/sdk/react/state/channels-and-threads/what-channels-and-threads-hold/): the participants and reply count in `thread.state`.
- [Typing in threads](https://getstream.io/chat/docs/sdk/react/features/messaging/typing-indicators/#typing-in-threads): the filter on `parent_id`.
- [Replies and threads](https://getstream.io/chat/docs/sdk/react/features/messaging/replies-and-threads/): how threads open and load.
- [Localization](https://getstream.io/chat/docs/sdk/react/features/app-experience/localization/): translating the reply count.
- [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.
- [Channel and thread side by side](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/channel-and-thread-side-by-side/): the thread panel beside the channel, with `ActiveThread`.
- [Channel header](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/headers/channel-header/): the same approach for the channel's header.

---

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