---
name: layout
description: "Shell and layout: AppShell slots, Panel and the PanelHeaderRoot vs PanelHeader distinction, AppRail vs Sidebar vs MobileNav, Resizable, ScrollArea, task surfaces."
---

# Layout and shell

## Contents

- Use the shell, do not rebuild it
- AppShell vs Panel vs Resizable
- AppRail vs Sidebar vs MobileNav
- Panel vs a bordered div
- ScrollArea vs overflow
- Per-component norms

---

## Use the shell, do not rebuild it

The application frame is `AppShellRoot` with named slots. Fill the slots.

```tsx
<AppShellRoot>
  <AppShellRailSlot>…</AppShellRailSlot>
  <AppShellSidebarSlot>…</AppShellSidebarSlot>
  <AppShellMainSlot>…</AppShellMainSlot>
</AppShellRoot>
```

Treat the slots as ownership markers, not required direct children. On desktop, put
`AppShellSidebarSlot` and `AppShellMainSlot` inside adjacent `ResizablePanel`s when the split is
user-adjustable. On mobile, one `AppShellMainSlot` can switch between the sidebar, main content,
and thread from app-owned route state.

AppShell slots own placement only. They do not add a divider, surface, width, inset, shadow, or
resize-handle offset. Put those details on the component rendered inside the slot. For a bounded
region, use `edge="attached"` on `Panel` (or a panel component that exposes the same prop) when it
meets the preceding region directly, and `edge="inset"` when it should read as an elevated inner
surface. The panel then owns its divider, header continuation, radius, and shadow. Set `overlay`
and render an `AppShellOverlaySlot` only to choose overlay placement; the overlay child still owns
its surface and edges.

**Incorrect** — reassembling the frame:

```tsx
<div className="flex h-screen">
  <aside className="w-16 border-r">…</aside>
  <aside className="w-64 border-r">…</aside>
  <main className="flex-1 overflow-auto">…</main>
</div>
```

That loses the responsive behavior, the sidebar collapse protocol, and the theme-family treatments. `AppShellSidebarTrigger` is the paired control for collapsing the sidebar — wire it rather than tracking your own boolean and toggling a width class.

---

## AppShell vs Panel vs Resizable

Three different jobs, often confused.

|                  | For                                                |
| ---------------- | -------------------------------------------------- |
| `AppShellRoot`   | the outermost slot layout — rail, sidebar, main    |
| `Panel`          | one bounded region with a header, body, and footer |
| `ResizableGroup` | regions whose split the **user** can drag          |

`Resizable` is only for user-adjustable splits. If the widths are fixed by design, use the shell slots or plain layout — do not wrap everything in a resizable group "just in case".

```tsx
<ResizableGroup orientation="horizontal">
  <ResizablePanel>…</ResizablePanel>
  <ResizableHandle />
  <ResizablePanel>…</ResizablePanel>
</ResizableGroup>
```

Every adjacent pair of `ResizablePanel`s needs a `ResizableHandle` between them. A group without handles is a layout, not a resizable.

---

## AppRail vs Sidebar vs MobileNav

|                 | For                                           |
| --------------- | --------------------------------------------- |
| `AppRailRoot`   | the narrow icon rail — top-level destinations |
| `SidebarRoot`   | the wide list — channels, DMs, sections       |
| `MobileNavRoot` | bottom navigation on small screens            |

`AppRail` composes `AppRailRoot`, `AppRailHeader`, `AppRailNav`, `AppRailFooter`, and items built from `AppRailItem` + `AppRailItemIcon` + `AppRailItemLabel`. Unread and attention signals have dedicated parts — `AppRailItemBadge`, `AppRailItemIndicator`, `AppRailItemAttention`, `AppRailItemAttentionMask`. Do not position a dot yourself.

`MobileNavRoot` composes `MobileNavItem` + `MobileNavLabel` for small screens. Render its items separately from the rail, but drive selection and navigation from the same app-owned route or view state. Mobile may expose only a subset of destinations; keep shared destinations aligned.

---

## Panel vs a bordered div

`Panel` is the standard bounded region, and it owns the whole header vocabulary.

```tsx
<Panel>
  <PanelHeaderRoot>…</PanelHeaderRoot>
  <PanelBody>…</PanelBody>
  <PanelFooter>…</PanelFooter>
</Panel>
```

**Two headers, pick by need:** `PanelHeaderRoot` is the bare frame — use it when you lay the header out yourself. `PanelHeader` is the composed identity chrome built on top of it, with `PanelHeaderContent` / `PanelHeaderIcon` / `PanelHeading` / `PanelTitle` / `PanelMeta` / `PanelActions` (`PanelAction`, `PanelToggleAction`) — use it when the surface has an identity row, as `ConversationPanel` does. Both type-check inside a `Panel`, so the wrong pick fails silently.

**`PanelStatus` vs `PanelActivity`:** `PanelStatus` is a standalone attribute badge in the header; `PanelActivity` is a live status readout wrapping a `Status`. Do not use one for the other.

When the body scrolls, use `PanelScrollViewport` + `PanelScrollContent` instead of putting `overflow-auto` on `PanelBody` — the scroll parts keep the header and footer pinned correctly. `PanelSection` divides the body into sections; `PanelSectionHeader` labels one.

Reach for `Panel` before writing `rounded-lg border border-line bg-layer-panel`.

Use `ProfilePanel` from `raft-ui` for a profile detail pane built on these parts.

---

## ScrollArea vs overflow

- Page-level scrolling → let the browser do it.
- A component with a specialized viewport → use that viewport: `PanelScrollViewport`,
  `TasksPanelViewport`, `MessageList`, or `SidebarContent`.
- A bounded custom region with no existing scroll owner → compose `ScrollArea`.

Use `ScrollArea` when the region itself owns scrolling and needs the library's scrollbar treatment.
Do not replace a specialized viewport just to get a custom scrollbar; the higher-level component
already owns the layout and overflow contract.

Keep one scroll owner per axis. Do not put `ScrollArea` inside `PanelScrollViewport` or
`TasksPanelViewport`, and do not make task columns or list sections independently scroll.

---

## Per-component norms

**`Tabs`** — see [forms.md](./forms.md) for `Tabs` vs `SegmentedControl`. Variants are `default` and `underline`. Use `TabsIndicator` and `TabsBackground` for the moving marker.

**`Separator`** — for dividing content within a region. Structural borders between shell regions come from the shell components themselves; do not add separators between slots.

**Task surfaces** — `TasksPanelRoot` is the outer surface (`TasksPanelToolbar`, `TasksPanelViewport`, `TasksPanelEmpty`). Inside it, choose:

| Component       | For                        |
| --------------- | -------------------------- |
| `TaskBoardRoot` | kanban columns             |
| `TaskListRoot`  | grouped list sections      |
| `TaskCard`      | one task in either surface |

`TasksPanelViewport` is the shared scroll owner for the complete panel. Put `TaskBoardRoot` or
`TaskListRoot` directly under it. A standalone Board or List demo may provide its own `ScrollArea`;
do not carry that wrapper into `TasksPanelViewport`.

`TaskBoardColumn` and `TaskSection` mirror each other — both have `Heading`, `Count`, `Badge`, `Trigger`, `Chevron`, `Panel`, `Items`, `Empty`. The board additionally distinguishes empty states by drop eligibility: `TaskBoardColumnEmptyIdle`, `TaskBoardColumnEmptyAllowed`, `TaskBoardColumnEmptyBlocked`.

Drag and drop is `TaskDndProvider` + `TaskDraggable` + `TaskDropZone` + `TaskDropPlaceholder` + `TaskDragOverlay`. Do not wire `@dnd-kit` directly.

`TaskCardLegacy` and the `LegacyTaskPanel*` family exist for old callsites — never use them in new code.
