# Thread 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 thread header of its own: the avatars of the people in the thread, the title, and a subtitle with the channel's name and the reply count, or who is typing a reply. The default header is explained in [Thread UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/thread-ui/#the-thread-header).

## 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 from `channel.state.typing`, filtered by the thread's id. `TypingIndicatorHeader` shows nothing inside a thread, because only the channel's composer receives typing events.
- Keep the header compact, so the replies keep the space.

## Create the component

```tsx title="src/CustomThreadHeader.tsx"
import type { ChannelStateData, EventPayload, ThreadState } from "stream-chat";
import {
  Avatar,
  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 });

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 { displayTitle } = useChannelPreviewInfo({ channel });

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

  return (
    <div className="custom-thread-header">
      <WorkspaceNavigationBackButton />
      <div className="custom-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="custom-thread-header__content">
        <div className="custom-thread-header__title">Thread</div>
        <div className="custom-thread-header__subtitle">
          {typingNames.length > 0
            ? `${typingNames.join(", ")} ${typingNames.length === 1 ? "is" : "are"} typing…`
            : `${displayTitle} · ${replyCount} replies`}
        </div>
      </div>
      <WorkspaceNavigationCloseButton />
    </div>
  );
};
```

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

.custom-thread-header__participants {
  display: flex;
  gap: 4px;
}

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

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

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

Here is what each part reads:

- `useThreadContext()` returns the thread that `Thread` or `ThreadSlot` renders. Its `state` holds the participants, each with a `user`, and the reply count. [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.
- `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`. The filter is the one from [Typing in threads](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/typing-indicators/#typing-in-threads), which also shows a typing indicator for the thread's message list.

The typing text here is plain English. To translate it, use `t` from `useTranslationContext()`, as [Localization](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/localization/) explains.

## Render it in the thread

`ThreadHeader` has no override key. `Thread` renders only its children, so put the header where `ThreadHeader` would go. With `ChatView`, that's inside `ThreadSlot`:

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

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

Render `ThreadPanel` in the views of `ChatView`, as in [Add threads](https://getstream.io/chat/docs/sdk/react/v15/getting-started/quick-start/#add-threads). Without a layout, render the header inside `<Thread thread={thread}>` the same way.

---

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