# Panel

> A column of the desktop main area with a 56px panel header and its own scrolling content; its header is also the top bar of a phone's pushed pages.

Source: https://raft-design.botiverse.build/components/panel. Generated from the current release; where it differs from what you remember, this page is right.

A panel is one column of the desktop main area: a panel header, then content that scrolls inside the column. Every column uses the same header, whether it holds a conversation, a thread, a side panel or a drawer's content. On a phone, `PanelHeader` is the top bar of a pushed page.

```tsx
import {
  Panel,
  PanelBody,
  PanelHeader,
  PanelHeaderContent,
  PanelHeading,
  PanelMeta,
  PanelScrollContent,
  PanelScrollViewport,
  PanelSection,
  PanelTitle,
  Text,
} from "@botiverse/raft-ui";
import type { PanelEdge } from "@botiverse/raft-ui";

export type PanelCompositionProps = {
  edge?: PanelEdge;
};

export function PanelComposition({ edge = "inset" }: PanelCompositionProps = {}) {
  return (
    <Panel edge={edge} className="flex size-full min-h-0  flex-col bg-layer-canvas">
      <PanelHeader className="border-b border-line-seam">
        <PanelHeaderContent>
          <PanelHeading>
            <PanelTitle>Workspace overview</PanelTitle>
          </PanelHeading>
          <PanelMeta>Botiverse</PanelMeta>
        </PanelHeaderContent>
      </PanelHeader>

      <PanelBody>
        <PanelScrollViewport>
          <PanelScrollContent>
            <PanelSection>
              <Text.Sans render={<h3 />} size="group">
                Description
              </Text.Sans>
              <Text.Sans className="mt-2" color="muted" size="small">
                Shared workspace for product, design, and engineering coordination.
              </Text.Sans>
            </PanelSection>

            <PanelSection>
              <Text.Sans render={<h3 />} size="group">
                Access
              </Text.Sans>
              <Text.Sans className="mt-2" size="small">
                Everyone in the organization can view this workspace.
              </Text.Sans>
            </PanelSection>

            <PanelSection>
              <Text.Sans render={<h3 />} size="group">
                Retention
              </Text.Sans>
              <Text.Sans className="mt-2" size="small">
                Messages and files are retained for one year.
              </Text.Sans>
            </PanelSection>

            <PanelSection>
              <Text.Sans render={<h3 />} size="group">
                Guest access
              </Text.Sans>
              <Text.Sans className="mt-2" size="small">
                Guests can view channels they have been invited to.
              </Text.Sans>
            </PanelSection>

            <PanelSection>
              <Text.Sans render={<h3 />} size="group">
                File uploads
              </Text.Sans>
              <Text.Sans className="mt-2" size="small">
                Workspace members can upload and share files.
              </Text.Sans>
            </PanelSection>
          </PanelScrollContent>
        </PanelScrollViewport>
      </PanelBody>
    </Panel>
  );
}
```

## When to use

| Situation                                                                              | Use                                                                              |
| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| A column of the desktop main area, or a side panel beside it (tasks, files, a profile) | Panel                                                                            |
| A conversation: header, tabs, messages and composer                                    | [ConversationPanel](https://raft-design.botiverse.build/components/conversation-panel.md), built on Panel |
| A thread beside a conversation                                                         | [ThreadPanel](https://raft-design.botiverse.build/components/thread-panel.md)                                   |
| The top bar of a phone's pushed page                                                   | `PanelHeader`                                                                    |
| The list of what a destination contains, next to the rail                              | [Sidebar](https://raft-design.botiverse.build/components/sidebar.md)                                                 |
| Details or settings that slide in over the page                                        | [Drawer](https://raft-design.botiverse.build/components/drawer.md)                                                    |

How the regions of a screen are arranged, and when a side panel sits beside the main area or covers it, is in [Page structure](https://raft-design.botiverse.build/patterns/page-structure.md) and [Responsive layout](https://raft-design.botiverse.build/patterns/responsive.md).

## Anatomy

- `Panel`: the column. `edge="attached"` draws a `line-seam` on its left edge, for a side panel next to a conversation; `edge="inset"` draws none.
- `PanelHeader`: the 56px header with a `line-seam` under it. Required.
  - `PanelHeaderIcon`: the location tile of a place, or the avatar of a person. Optional.
  - `PanelHeaderContent`: holds `PanelHeading` and `PanelMeta`.
  - `PanelHeading`: the `PanelTitle`, followed by any `PanelStatus` (a tag such as Archived) or `PanelActivity` (a live status, wrapping a [Status](https://raft-design.botiverse.build/components/status.md)).
  - `PanelMeta`: one line of description under the title.
  - `PanelActions`: the trailing `PanelAction` and `PanelToggleAction` buttons.
- `PanelSectionHeader`: the same bar for a section-level view. On a phone it sits on the frame.
- `PanelHeaderRoot`: the bare 56px bar both headers are built on, for a header with its own layout.
- `PanelNotice`, with `PanelNoticeContent` and `PanelNoticeActions`: a full-width strip under the header for a problem in this panel. When to use it instead of a banner is in [Banner](https://raft-design.botiverse.build/components/banner.md). A status pixel or an icon placed before `PanelNoticeContent` leads its first line ([A mark that leads text](https://raft-design.botiverse.build/foundations/alignment.md#a-mark-that-leads-text)).
- `PanelBody`, `PanelScrollViewport` and `PanelScrollContent`: the content and its single scroll container.
- `PanelSection`: a block of content with a hairline above it.
- `PanelFooter`: the bottom action area, padded for the safe area.

```tsx
import { BellOff, Hash, Search, Settings } from "lucide-react";
import {
  Avatar,
  AvatarGroup,
  PanelAction,
  PanelActions,
  PanelHeader,
  PanelHeaderContent,
  PanelHeaderIcon,
  PanelHeading,
  PanelMeta,
  PanelActivity,
  PanelStatus,
  PanelTitle,
  Status,
} from "@botiverse/raft-ui";

export type ConversationPanelHeaderCompositionProps = {
  conversation?: "channel" | "dm";
  muted?: boolean;
};

export function ConversationPanelHeaderComposition({
  conversation = "channel",
  muted = false,
}: ConversationPanelHeaderCompositionProps) {
  if (conversation === "dm") {
    return (
      <PanelHeader>
        <PanelHeaderContent>
          <Avatar data-slot="panel-header-icon" size="md" type="agent" />
          <PanelHeading>
            <PanelTitle>Bugen</PanelTitle>
            <PanelActivity title="Working…">
              <Status aria-hidden size="sm" presence="busy" />
              Working…
            </PanelActivity>
          </PanelHeading>
        </PanelHeaderContent>
        <PanelActions>
          <PanelAction aria-label="Search this conversation">
            <Search />
          </PanelAction>
          <PanelAction aria-label="Conversation details and settings">
            <Settings />
          </PanelAction>
        </PanelActions>
      </PanelHeader>
    );
  }

  return (
    <PanelHeader>
      <PanelHeaderIcon>
        <Hash />
      </PanelHeaderIcon>
      <PanelHeaderContent>
        <PanelHeading>
          <PanelTitle>launch-week</PanelTitle>
          {muted ? (
            <PanelStatus title="Activity muted. Direct mentions can still notify you.">
              <BellOff />
            </PanelStatus>
          ) : null}
        </PanelHeading>
        <PanelMeta>
          <span className="flex min-w-0 items-center gap-1.5">
            <AvatarGroup aria-hidden>
              <Avatar size="xs" type="agent" />
              <Avatar size="xs" type="agent" />
            </AvatarGroup>
            <span className="shrink-0">2 agents working</span>
            <span aria-hidden>·</span>
            <span className="truncate">Ship the launch week</span>
          </span>
        </PanelMeta>
      </PanelHeaderContent>
      <PanelActions>
        <PanelAction aria-label="Search this channel">
          <Search />
        </PanelAction>
        <PanelAction aria-label="Channel details and settings">
          <Settings />
        </PanelAction>
      </PanelActions>
    </PanelHeader>
  );
}
```

## Variants

| Header               | Use for                                                                                                                |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `PanelHeader`        | Every desktop panel header; the white top bar of a phone's pushed page                                                 |
| `PanelSectionHeader` | A section-level view. On a phone it sits on `layer-frame` with `foreground-on-frame` text, and hides `PanelHeaderIcon` |

A phone's pushed page has a white top bar, and its location tile stays a yellow sticker ([Open questions](https://raft-design.botiverse.build/decisions/open-questions.md#settled-by-the-product)).

## States

- A pressed `PanelToggleAction`, or a `PanelAction` with `aria-pressed`, is a yellow sticker: it marks the side panel that is open.
- Other states follow [States](https://raft-design.botiverse.build/foundations/states.md).

## Behavior

- The header stays put; only `PanelScrollViewport` scrolls.
- The title and the description truncate with an ellipsis on one line each.
- On a phone the header sticks to the top of the page.
- When the window is shorter than 600px, the header shrinks with every other header ([Spacing](https://raft-design.botiverse.build/foundations/spacing.md#heights)).

### Phone top bars

The structure of a phone's top bars (the navigation row of a top-level page, Back and up to 2 icon buttons on a pushed page) is in [Page structure](https://raft-design.botiverse.build/patterns/page-structure.md#phone). The top bar also follows these rules:

- On a top-level page, put the page name in the 56px row in `text-headline`, with no large title and no icon tile before it ([Page structure](https://raft-design.botiverse.build/patterns/page-structure.md#phone)).
- Don't put New or Search buttons in the top bar of a top-level page: search is the Search tab ([Navigation](https://raft-design.botiverse.build/patterns/navigation.md#search-on-a-phone)).
- Make the bell the entry to the notification center, with a pink pixel when something is new. On a phone it is only in the top bar of the first top-level page ([Page structure](https://raft-design.botiverse.build/patterns/page-structure.md#phone)).
- Use the `arrow-left` icon for Back, not a chevron: its ink fills the icon box like other icons, so it mirrors the button at the other end without being shifted ([Alignment](https://raft-design.botiverse.build/foundations/alignment.md#the-ends-of-a-header)).

## Content

- Name the place, the person or the view in `PanelTitle`.
- Put a location tile ([IconTile](https://raft-design.botiverse.build/components/icon-tile.md)) in `PanelHeaderIcon` for a place, and the person's avatar for a person. Other pages have no icon: their `PanelMeta` says where they belong, such as the channel a thread is in.
- Start `PanelMeta` with what is live, then the counts: "2 agents working · 5 members" ([Working agents](https://raft-design.botiverse.build/people-and-agents/working-agents.md#where-working-shows)). Set numbers and IDs in Space Mono ([Typography](https://raft-design.botiverse.build/foundations/typography.md#usage)).
- Put a tag on the name (Experimental, Archived, Deleted) in `PanelHeading` as a `PanelStatus`, never in `PanelMeta`: the description truncates, and truncation cuts off the tag's edge.

## Accessibility

- `Panel` renders a `section`. Name it with `aria-label` or `aria-labelledby` when several panels sit side by side.
- `PanelTitle` renders a `div`. Render it as the page's heading (`render={<h1 />}`, or `h2` inside a page that has one).
- `PanelAction` wraps the Base UI Button and always renders `type="button"`. `PanelToggleAction` wraps the Base UI Toggle, which sets `aria-pressed`.
- Give every icon-only `PanelAction` and `PanelToggleAction` an `aria-label`: "Back", "Channel Details", "Close Tasks".
- Give an icon-only `PanelStatus` (a muted bell) an accessible name.

## Rules

- Use this header in every panel: conversation, thread, side panel and drawer. Don't give one panel its own header or button sizes.
- Use `PanelAction` for the header's icon buttons: 32px with an 18px icon and a 44px hit area on desktop (from 768px with a mouse or trackpad), 4px apart; 44px with a 22px icon, 2px apart, on a phone and on a touch screen at any width. They follow the default control height ([Spacing](https://raft-design.botiverse.build/foundations/spacing.md#heights)). A 44px button next to a 17px title looks heavy on desktop; a finger needs 44px.
- Put Back first in the header and the actions last, in `PanelActions`: the header pulls its first and last button out so their icon boxes, not the buttons, sit on the content margin, 20px on a desktop and on a touch screen in the desktop layout, 16px on a phone, at every button size ([Alignment](https://raft-design.botiverse.build/foundations/alignment.md#the-ends-of-a-header)). Don't add margins of your own to move an edge button. Keep only `PanelAction`s in `PanelActions`: the group declares their inset, so an action hidden at some width (a thread's Close below 1024px) leaves the next one on the edge. A frameless `Button` at `icon-md` or `icon-lg` as the header's first or last child is pulled the same way; a filled one ends its shape on the margin.
- Give a `PanelStatus` its own cell on the title's line, and center it by its shape. Don't nudge it: the title's letters already sit on the line's center, so a 1px shift makes it sit high.
- Open a side panel from the header with a `PanelToggleAction`: pressing it again closes the panel.
- Give a side panel next to a conversation `edge="attached"`, so it draws the one line between the two ([Page structure](https://raft-design.botiverse.build/patterns/page-structure.md#one-line-between-two-regions)).

## API

### Panel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `edge` | `PanelEdge` |  |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelAction

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `focusableWhenDisabled` | `boolean` | `false` | Whether the button should be focusable when disabled. |
| `nativeButton` | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelActions

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelActivity

Live status readout in a panel header, such as agent activity or presence. Wraps a `Status` and supplies header-appropriate spacing and typography; use `PanelStatus` for a standalone attribute badge.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelBody

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelFooter

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelHeader

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelHeaderContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelHeaderIcon

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelHeaderRoot

Bare header bar: fixed height and centered flex row, no border, fill, or padding. Shared base for `PanelHeader`, `PanelSectionHeader`, and panel-specific headers such as `ThreadPanelHeader` — it is not exclusive to `PanelHeader`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelHeading

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelMeta

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "md"` | `"sm"` |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelNotice

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `status` | `PanelNoticeStatus` | `"warning"` | The tint: warning for something to fix (the default), destructive only for what cannot be undone. |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelNoticeActions

Trailing controls (copy, a link, ×), centred on the first text line.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelNoticeContent

The message: takes the width left and wraps or clamps as the caller says.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelScrollContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelScrollViewport

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelSection

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelSectionHeader

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelStatus

Attribute badge in a panel header, such as a muted or archived marker. Use `PanelActivity` for a live status readout wrapping a `Status`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### PanelToggleAction

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultPressed` | `boolean` | `false` | Whether the toggle button is currently pressed. This is the uncontrolled counterpart of `pressed`. |
| `disabled` | `boolean` | `false` | Whether the component should ignore user interaction. |
| `nativeButton` | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `onPressedChange` | `(pressed: boolean, eventDetails: BaseToggle.ChangeEventDetails) => void` |  | Callback fired when the pressed state is changed. |
| `pressed` | `boolean` |  | Whether the toggle button is currently pressed. This is the controlled counterpart of `defaultPressed`. |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |
| `value` | `string` |  | A unique string that identifies the toggle when used inside a toggle group. |

Also takes the attributes of its element.

## Changes

- 0.10.2 (Fixed): `PanelAction` and `PanelToggleAction` follow the same desktop condition as every other control (`(width >= 48rem) and (pointer: fine)`). On a touch tablet they were 32px while the rest stayed 44px; they now keep 44px with 22px icons there. Phone and desktop sizes are unchanged.
- 0.5.40 (Fixed): Elegant `PanelHeader`'s icon chip: `fill-muted` measured 1.04:1 against the canvas-muted header (effectively invisible) and becomes `fill-strong`; avatar slots declare `data-icon-kind="avatar"` and drop the chip entirely, so a round avatar no longer sits on a square. Brutal's black-bordered chip is unchanged.
- 0.5.36 (Changed): Panel alignment: the elegant `PanelHeader` puts its icon box on the 20px line and the title on the 68px line (36px to 20px at `md`+, gap 8 to 12); `MessageItem` rows in both themes carry the 8px inset (7px padding + 1px border, 6px + 2px in Brutal) that lands the avatar on 20 and the text on 68 next to the 12px the app shell keeps. Brutal also drops the row's `mx-3`, taking the mobile avatar inset from 32 to 20. The thread panel uses the chat panel's figures.
- 0.5.0 (Breaking Changes): **Elegant `PanelHeaderIcon` now owns its container treatment.** It renders as a 36px rounded muted surface instead of a bare auto-sized slot. Remove redundant consumer wrappers and update layouts that relied on the previous intrinsic size.
- 0.4.0 (Breaking Changes): **`PanelHeaderIcon` no longer sizes its icon.** The slot dropped the `[&_svg]` sizing (brutal 18px / elegant 14px stroke 2.5); consumers must now size icons themselves, e.g. with a `size-*` class or the `size` attribute. ([0295c2e](https://github.com/botiverse/rui/commit/0295c2e))
- 0.3.0 (Breaking Changes): Renamed `PanelHeaderFrame` to `PanelHeaderRoot` and updated the corresponding type and `data-slot` from `panel-header-frame` to `panel-header-root`. Panel header parts remain available from the package root and are now owned by the shared Panel vocabulary.
- 0.3.0 (Added): Added `AppShellPanelSlot` and `AppShellOverlaySlot`, overlay-aware main slots, attached/inset Panel edges, and the shared `PanelActivity` header part for composable multi-panel layouts.
- 0.2.0 (Breaking Changes): Generalized panel chrome: replace `PanelHeader` with `PanelHeaderFrame`, and replace `ConversationPanelActions`, `ConversationPanelHeader*`, `ConversationPanelHeading`, `ConversationPanelMeta`, `ConversationPanelSectionHeader`, `ConversationPanelStatus`, and `ConversationPanelTitle` with their `Panel*` counterparts. Update selectors targeting the renamed `data-slot` values.
