# Channel details

Channel details is the SDK's panel for one channel: its name and image, its members, its pinned messages, and the photos and files shared in it. The user can mute the channel and leave it there.

>
> **Note:** **What you'll build**
>
> - The channel's title as a button that opens the details
> - Channel details in the side panel instead of a modal
> - A members view with online members first
> - A restyled card of actions
>

---

**Preview**

![The channel details in the side panel beside the Design review channel, opened from the channel's title in the header, with the group's name, avatar, member count and the Mute, Leave and Delete actions](https://getstream.io/docs-assets/images/7ca8947d86c2.png)

---

**Approach:** Compose, Replace one part and Restyle. [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), with its [side panel](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/overview/#the-side-panel). The page shows only what the recipe adds: the panel's body, the header whose title opens it, and the stylesheets.

## Best practices

- Start from the SDK's `<ChannelDetail/>`, from `stream-chat-react/channel-detail`. It already handles editing, muting, leaving, deleting and the members, and checks the user's capabilities for each action. [Channel details and members](https://getstream.io/chat/docs/sdk/react/features/channels/channel-details-and-members/) lists what each section calls.
- When a modal is enough, pass `AvatarWithChannelDetail` to `<ChannelHeader/>` as its `Avatar` and skip [Channel details in the side panel](#channel-details-in-the-side-panel). The avatar then opens the details in a modal.
- Give `<ChannelDetail/>` the channel through `<ChannelInstanceProvider/>`. The side panel sits outside the channel's `<Channel/>`, and the Channel info section reads the channel from that context for its online status.
- Make the control that opens the panel a real `button`, even when it looks like text such as the channel's title. A `button` takes focus and answers Enter and Space, and needs a name that says what it opens and an `aria-expanded` that says whether it's open.
- Pass `<ChannelDetail/>` an `onClose` that closes your panel, as [Render it in a side panel](https://getstream.io/chat/docs/sdk/react/component-reference/channel-detail/channel-detail/#render-it-in-a-side-panel) explains.
- Change the panel's look through tokens, set on your own element around it. The SDK's rules keep working, and the panel follows your theme.

## Channel details in the side panel

### Loading the stylesheet

`ChannelDetail` has its own stylesheet. Put it in the same layer as the SDK's main stylesheet, so the rules in `stream-overrides` win over both:

```css title="src/index.css"
@layer stream, stream-overrides;

@import "stream-chat-react/dist/css/index.css" layer(stream);
@import "stream-chat-react/dist/css/channel-detail.css" layer(stream);
```

### The panel's body and its toggle

`<ChannelDetails/>` renders `<ChannelDetail/>` for the channel in the `channel` slot, so it follows the user to another channel. `useChannelDetailsPanel()` returns `toggle`, which opens it in the side panel with `useSidePanel().openSidePanel()` and closes the side panel when the details are the open content, and `isOpen`, for the control that calls it:

```tsx label="React" title="src/ChannelDetailsPanel.tsx"
import { useMemo } from "react";
import { ChannelInstanceProvider } from "stream-chat-react";
import { ChannelDetail } from "stream-chat-react/channel-detail";
import type { SectionNavigatorSection } from "stream-chat-react/channel-detail";
import { useSlotChannel } from "stream-chat-react/slot-layout";
import { useSidePanel } from "./SidePanel";
import "./ChannelDetailsPanel.css";

const CHANNEL_DETAILS_ID = "channel-details";

type ChannelDetailsProps = {
  /** The panel's sections, in order. Defaults to the SDK's five. */
  sections?: SectionNavigatorSection[];
};

/**
 * The SDK's `ChannelDetail` for the channel open in the `channel` slot, so it follows the user to
 * another channel.
 */
const ChannelDetails = ({ sections }: ChannelDetailsProps) => {
  const channel = useSlotChannel({ slot: "channel" });
  const { closeSidePanel } = useSidePanel();
  const channelContext = useMemo(
    () => (channel ? { channel } : undefined),
    [channel],
  );

  if (!channelContext) return null;

  return (
    // Channel info reads the channel from the channel context for its online status, and the
    // side panel sits outside the channel's `Channel`.
    <ChannelInstanceProvider value={channelContext}>
      <div className="cookbook-channel-details">
        {/* A new channel starts the panel again on its first section. The sections' close
            button, and actions such as opening a pinned message, close the side panel. */}
        <ChannelDetail
          channel={channelContext.channel}
          key={channelContext.channel.cid}
          onClose={closeSidePanel}
          sections={sections}
        />
      </div>
    </ChannelInstanceProvider>
  );
};

/**
 * Opens the channel's details in the side panel, and closes them when they're the open content.
 * `isOpen` tells the control that toggles them whether the details are on screen.
 */
export const useChannelDetailsPanel = ({
  sections,
}: ChannelDetailsProps = {}) => {
  const { closeSidePanel, openId, openSidePanel } = useSidePanel();
  const isOpen = openId === CHANNEL_DETAILS_ID;

  const toggle = () => {
    if (isOpen) closeSidePanel();
    else
      openSidePanel({
        body: <ChannelDetails sections={sections} />,
        id: CHANNEL_DETAILS_ID,
        // ChannelDetail brings its own header, with the section's title and a close button.
        ownHeader: true,
        title: "Channel details",
      });
  };

  return { isOpen, toggle };
};
```

```css label="CSS" title="src/ChannelDetailsPanel.css"
@layer stream-overrides {
  .cookbook-channel-details {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-height: 0;
  }

  /* In a modal, the panel sets its own size. Here, it fills the side panel. */
  .cookbook-channel-details .str-chat__channel-detail {
    width: 100%;
    height: 100%;
  }
}
```

In a modal, `<ChannelDetail/>` is 800 by 640 pixels, and in its narrow layout it takes the whole window. The last rule makes it fill the side panel instead. At that width, `<ChannelDetail/>` uses its narrow layout: one section at a time, with a menu button in its header that opens the list of sections.

That header has the section's title and its own close button, so `useChannelDetailsPanel()` opens the details with `ownHeader: true`: the side panel leaves its title row out. `<ChannelDetails/>` passes `useSidePanel().closeSidePanel` to `<ChannelDetail/>` as `onClose`, so the section's close button closes the side panel.

## Replace the members view

The Members section lists the members with their role and presence. This view shows the online members first, each with an Online badge, and when the others were last active. It reads the members from `channel.state.members`, and keeps the SDK's member profile: clicking a member opens `<ChannelMemberDetail/>`, with a back button to the list.

---

**Online members first**

![The members view: Bob, Cara and the user with an Online badge, and Dan, with when he was last seen and an Offline badge](https://getstream.io/docs-assets/images/fc305f98f7ff.png)

---

```tsx label="React" title="src/MembersWithPresence.tsx"
import { useId, useState } from "react";
import { convertTimestampToDate } from "stream-chat";
import type { ChannelMemberResponse, ChannelStateData } from "stream-chat";
import {
  Avatar,
  Prompt,
  useChatContext,
  useStateStore,
} from "stream-chat-react";
import {
  ChannelMemberDetail,
  defaultChannelDetailSections,
  SectionNavigatorHeader,
  useChannelDetailContext,
} from "stream-chat-react/channel-detail";
import type {
  SectionNavigatorSection,
  SectionNavigatorSectionContentProps,
} from "stream-chat-react/channel-detail";
import "./MembersWithPresence.css";

const membersSelector = ({ members }: ChannelStateData) => ({ members });

// The panel's own text is English, so its dates are too. An app in another language passes its
// language here.
const relativeTime = new Intl.RelativeTimeFormat("en-US", { numeric: "auto" });

// "Last seen 5 minutes ago", in the largest unit that fits.
const formatLastSeen = (lastActive: Date) => {
  const minutes = Math.round((lastActive.getTime() - Date.now()) / 60_000);
  if (Math.abs(minutes) < 60)
    return `Last seen ${relativeTime.format(minutes, "minute")}`;
  const hours = Math.round(minutes / 60);
  if (Math.abs(hours) < 24)
    return `Last seen ${relativeTime.format(hours, "hour")}`;
  return `Last seen ${relativeTime.format(Math.round(hours / 24), "day")}`;
};

const getName = (member: ChannelMemberResponse) =>
  member.user?.name || member.user?.id || member.user_id || "";

type MemberRowProps = {
  isOwnUser: boolean;
  member: ChannelMemberResponse;
  onSelect: (member: ChannelMemberResponse) => void;
};

const MemberRow = ({ isOwnUser, member, onSelect }: MemberRowProps) => {
  const { user } = member;
  const name = getName(member);
  const online = !!user?.online;
  const lastActive = convertTimestampToDate(user?.last_active);
  const hintId = useId();

  // The row's text names the button, status included; the hint adds what clicking it does.
  return (
    <li>
      <button
        aria-describedby={hintId}
        className="cookbook-member-row"
        onClick={() => onSelect(member)}
        type="button"
      >
        <Avatar
          imageUrl={user?.image}
          isOnline={online}
          size="md"
          userName={name}
        />
        <span className="cookbook-member-row__text">
          <span className="cookbook-member-row__name">
            {name}
            {isOwnUser && (
              <span className="cookbook-member-row__you"> (you)</span>
            )}
          </span>
          {!online && lastActive && (
            <span className="cookbook-member-row__last-seen">
              {formatLastSeen(lastActive)}
            </span>
          )}
        </span>
        <span
          className={`cookbook-member-row__status cookbook-member-row__status--${
            online ? "online" : "offline"
          }`}
        >
          {online ? "Online" : "Offline"}
        </span>
        <span hidden id={hintId}>
          Opens {name}&apos;s details
        </span>
      </button>
    </li>
  );
};

/**
 * The Members section with each member's online status: who is online first, then everyone
 * else. It reads the channel's members from `channel.state`, which the client updates when a
 * member comes online or goes offline.
 */
export const MembersWithPresence = ({
  layout,
}: SectionNavigatorSectionContentProps) => {
  const { client } = useChatContext();
  const { channel, close } = useChannelDetailContext();
  const { members } = useStateStore(channel.state, membersSelector);
  const [selectedMember, setSelectedMember] = useState<ChannelMemberResponse>();

  if (selectedMember) {
    return (
      <ChannelMemberDetail
        layout={layout}
        member={selectedMember}
        onBack={() => setSelectedMember(undefined)}
      />
    );
  }

  const sorted = Object.values(members).sort(
    (a, b) =>
      Number(!!b.user?.online) - Number(!!a.user?.online) ||
      getName(a).localeCompare(getName(b)),
  );
  const onlineCount = sorted.filter((member) => member.user?.online).length;

  return (
    <div className="cookbook-members-with-presence">
      <SectionNavigatorHeader
        close={close}
        title={`${sorted.length} members · ${onlineCount} online`}
      />
      <Prompt.Body>
        <ul className="cookbook-members-with-presence__list">
          {sorted.map((member) => (
            <MemberRow
              isOwnUser={member.user?.id === client.userId}
              key={member.user?.id ?? member.user_id}
              member={member}
              onSelect={setSelectedMember}
            />
          ))}
        </ul>
      </Prompt.Body>
    </div>
  );
};

// The default sections, with this view in place of the Members section's content.
export const sectionsWithPresence: SectionNavigatorSection[] =
  defaultChannelDetailSections.map((section) =>
    section.id === "channel-members"
      ? { ...section, SectionContent: MembersWithPresence }
      : section,
  );
```

```css label="CSS" title="src/MembersWithPresence.css"
@layer stream-overrides {
  .cookbook-members-with-presence {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-height: 0;
  }

  .cookbook-members-with-presence__list {
    margin: 0;
    padding: 0 var(--str-chat__spacing-xs);
    overflow-y: auto;
    list-style: none;
  }

  .cookbook-member-row {
    display: flex;
    align-items: center;
    gap: var(--str-chat__spacing-sm);
    width: 100%;
    padding: var(--str-chat__spacing-xs) var(--str-chat__spacing-sm);
    border: 0;
    border-radius: var(--str-chat__radius-lg);
    background: none;
    color: var(--str-chat__text-primary);
    text-align: start;
    cursor: pointer;
  }

  .cookbook-member-row:hover {
    background: var(--str-chat__background-utility-hover);
  }

  .cookbook-member-row__text {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-width: 0;
  }

  .cookbook-member-row__name {
    font: var(--str-chat__font-body-emphasis);
  }

  .cookbook-member-row__you,
  .cookbook-member-row__last-seen {
    color: var(--str-chat__text-secondary);
    font: var(--str-chat__font-caption-default);
  }

  .cookbook-member-row__status {
    padding: var(--str-chat__spacing-xxxs) var(--str-chat__spacing-xs);
    border: 1px solid var(--str-chat__border-core-default);
    border-radius: var(--str-chat__radius-max);
    font: var(--str-chat__font-caption-emphasis);
  }

  .cookbook-member-row__status--online {
    border-color: var(--str-chat__border-utility-success);
    color: var(--str-chat__accent-success);
  }

  .cookbook-member-row__status--offline {
    color: var(--str-chat__text-tertiary);
  }
}
```

`<SectionNavigatorHeader/>` is the SDK's section header. It shows the menu button in the narrow layout, so the user can still reach the other sections. The view replaces only the section's `SectionContent`; its id and nav button stay the SDK's.

The list renders every member the channel has loaded, without a search field or paging. For a channel with many members, keep the SDK's view, which searches the members on the server and loads them page by page.

## Make the channel's title open the panel

`<ChannelHeader/>` takes a `Title` component that replaces only the title. It receives the channel and its display title. The back button, the subtitle, the avatar and the close button stay the SDK's.

The title is a `button` that calls `toggle` from `useChannelDetailsPanel()`, here with the members view above. Leave out `sections` for the SDK's five sections. It keeps the SDK's `str-chat__channel-header__data__title` class, so it looks like the title it replaces. The button's name, "Channel details for Design review", says what it opens and includes the visible title, so voice control finds it by what it shows, and `aria-expanded` tells assistive technology whether the details are open. The stylesheet takes away the button's chrome, underlines the title on hover, and draws the focus ring inside the button, since the header's title column clips anything outside it:

```tsx label="React" title="src/ChannelHeaderWithDetails.tsx"
import type { ChannelHeaderTitleProps } from "stream-chat-react";
import { ChannelHeader } from "stream-chat-react";
import { useChannelDetailsPanel } from "./ChannelDetailsPanel";
import { sectionsWithPresence } from "./MembersWithPresence";
import "./ChannelHeaderWithDetails.css";

/** The channel's title as a button that opens the channel's details in the side panel, and closes them. */
const TitleThatOpensDetails = ({ displayTitle }: ChannelHeaderTitleProps) => {
  const { isOpen, toggle } = useChannelDetailsPanel({
    sections: sectionsWithPresence,
  });

  return (
    // The name keeps the visible title, so voice control finds the button by what it shows.
    <button
      aria-expanded={isOpen}
      aria-label={`Channel details for ${displayTitle}`}
      className="str-chat__channel-header__data__title cookbook-channel-title-button"
      onClick={toggle}
      type="button"
    >
      {displayTitle}
    </button>
  );
};

export const ChannelHeaderWithDetails = () => (
  <ChannelHeader Title={TitleThatOpensDetails} />
);
```

```css label="CSS" title="src/ChannelHeaderWithDetails.css"
@layer stream-overrides {
  /* The title keeps the header's look: no button chrome, only an underline on hover. */
  .cookbook-channel-title-button {
    padding: 0 var(--str-chat__spacing-xs);
    border: 0;
    border-radius: var(--str-chat__radius-sm);
    background: none;
    color: inherit;
    cursor: pointer;
  }

  .cookbook-channel-title-button:hover {
    text-decoration: underline;
  }

  /* Drawn inside the button: the header's title column clips anything outside it. */
  .cookbook-channel-title-button:focus-visible {
    outline: 2px solid var(--str-chat__accent-primary);
    outline-offset: -2px;
  }
}
```

Use it in place of `<ChannelHeader/>` in the base app's channels view:

```tsx
<ChannelSlot slot="channel">
  <ChannelHeaderWithDetails />
  <MessageList />
  <MessageComposer />
</ChannelSlot>
```

## Restyle it with tokens

The panel's rules read the SDK's tokens, so setting tokens on `.cookbook-channel-details`, the element around `<ChannelDetail/>`, restyles it without matching the SDK's selectors. This stylesheet gives the card of actions larger corners through `--str-chat__switch-field-border-radius`; its background stays the SDK's card surface, which stands out on the side panel. The screenshot at the top of this page shows the result.

```css title="src/channel-details-theme.css"
@layer stream-overrides {
  /* The card of actions gets larger corners; it keeps the SDK's card background. */
  .cookbook-channel-details {
    --str-chat__switch-field-border-radius: var(--str-chat__radius-xl);
  }
}
```

## How it works

>
> **Info:** **Example**
>
> The user opens Design review and clicks its title, Design review, in the header:
>
> 1. The title calls `toggle` from `useChannelDetailsPanel()`, which calls `useSidePanel().openSidePanel()` with `<ChannelDetails/>` as the body. The side panel shows it after the channel, and after the thread when one is open, in place of anything it showed.
> 2. The panel opens on Channel info: the group's avatar and name, the member and online counts, and the Mute chat switch, Leave chat and Delete chat, which the user's capabilities allow.
> 3. The user opens the menu and picks Members. `<MembersWithPresence/>` reads `channel.state.members` and lists the online members first, and when the others were last seen.
> 4. A member comes online. The client updates their `member.user` in `channel.state.members`, and the list moves them up with an Online badge.
> 5. The user opens Book club. `<ChannelDetails/>` reads the new channel from the `channel` slot and starts again on Channel info for Book club.
> 6. The user clicks the close button in the section's header. It calls `useSidePanel().closeSidePanel()`, the `onClose` that `<ChannelDetails/>` passes to `<ChannelDetail/>`. The side panel closes, and focus goes back to the title. Clicking the title again closes the panel too.
>

The switch and the buttons in Channel info call the state layer: the switch calls `Channel.mute()` and `Channel.unmute()`, and Leave chat asks the user to confirm, then calls `Channel.removeMembers()` with the user's id. Opening a pinned message also calls `close` from `useChannelDetailContext()`, and the channel's message list jumps to it.

The header's online count and the members list can disagree. The header counts `channel.state.watcherCount`, the users watching the channel now. The list reads `member.user.online`, which is true while the member is connected to your app anywhere.

## Related

- [Channel details and members](https://getstream.io/chat/docs/sdk/react/features/channels/channel-details-and-members/): what each section does and calls, and who sees which action.
- [Channel Detail](https://getstream.io/chat/docs/sdk/react/component-reference/channel-detail/channel-detail/): the `ChannelDetail` props, its sections and its layouts.
- [Channel Detail Views](https://getstream.io/chat/docs/sdk/react/component-reference/channel-detail/channel-detail-views/): `ChannelMembersView`, `ChannelMemberDetail` and the other views.
- [Channel details actions](https://getstream.io/chat/docs/sdk/react/ui/components/actions/#channel-details-actions): removing, adding and replacing the panel's actions.
- [Theming](https://getstream.io/chat/docs/sdk/react/features/app-experience/theming/#css-layers): CSS layers and the tokens.
- [Members and online status](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/channel-list/members-and-online-status/): the members and their online status in the channel header.
- [Channel and thread side by side](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/channel-and-thread-side-by-side/): the channel and thread slots the side panel sits after.

---

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