# AppShell

> The outermost layout of an app, with slots for the rail, the sidebar, the main area, a side panel and an overlay, that switches to a single column with the tab bar below 768px.

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

AppShell places the regions of an app screen: on desktop the rail, the sidebar, the main area and a side panel from left to right; below 768px the main area alone, with the tab bar under it. Its slots own placement only. What each region looks like belongs to the component inside it. Which regions a screen has, and how they change with width, is in [Page structure](https://raft-design.botiverse.build/patterns/page-structure.md) and [Responsive layout](https://raft-design.botiverse.build/patterns/responsive.md).

```tsx
import {
  AppShellMainSlot,
  AppShellRailSlot,
  AppShellRoot,
  AppShellSidebarSlot,
  MobileNavItem,
  MobileNavLabel,
  MobileNavRoot,
} from "@botiverse/raft-ui";

export function AppShellResponsiveComposition() {
  return (
    <AppShellRoot className="min-h-dvh">
      <AppShellRailSlot>
        <nav className="w-16" aria-label="Workspace navigation">
          Rail
        </nav>
      </AppShellRailSlot>

      <AppShellSidebarSlot>
        <div className="w-60">Conversation list</div>
      </AppShellSidebarSlot>

      <AppShellMainSlot>
        <div>Conversation</div>
      </AppShellMainSlot>

      <MobileNavRoot className="order-last md:hidden" aria-label="Primary navigation">
        <MobileNavItem selected>
          <MobileNavLabel>Messages</MobileNavLabel>
        </MobileNavItem>
      </MobileNavRoot>
    </AppShellRoot>
  );
}
```

## When to use

| Situation                                             | Use                                                                      |
| ----------------------------------------------------- | ------------------------------------------------------------------------ |
| The outer frame of an app screen                      | AppShell                                                                 |
| One bounded region with a header, a body and a footer | [Panel](https://raft-design.botiverse.build/components/panel.md), inside a slot                                |
| A split people can drag                               | [Resizable](https://raft-design.botiverse.build/components/resizable.md), with the slots inside its panels |
| The destinations on desktop                           | [AppRail](https://raft-design.botiverse.build/components/app-rail.md), in `AppShellRailSlot`                |
| The destinations on a phone                           | [MobileNav](https://raft-design.botiverse.build/components/mobile-nav.md), after `AppShellMainSlot`       |

## Anatomy

- `AppShellRoot`: the frame, on `layer-canvas`. A row from 768px, a column below it. Required.
- `AppShellRailSlot`: an `aside` for the [AppRail](https://raft-design.botiverse.build/components/app-rail.md). Hidden below 768px.
- `AppShellSidebarSlot`: an `aside` for the [Sidebar](https://raft-design.botiverse.build/components/sidebar.md). Hidden below 768px.
- `AppShellMainSlot`: the `main` element, for the main area. The only slot shown on a phone. Required.
- `AppShellPanelSlot`: an `aside` for a side panel next to the main area. Hidden below 768px.
- `AppShellOverlaySlot`: covers the main area without changing the columns, for a side panel that overlays it. Put it inside an `AppShellMainSlot` with `overlay`.
- `AppShellSidebarTrigger`: the button that collapses and expands the sidebar. It is a `PanelAction` for the main area's panel header; render it as an `AppRailItem` to put it in the rail, where the products put it ([Navigation](https://raft-design.botiverse.build/patterns/navigation.md#collapsing-the-sidebar)).

The slots are placement markers, not required direct children: a slot can sit inside a `ResizablePanel`.

## Variants

| Variant                                | Use for                                                        |
| -------------------------------------- | -------------------------------------------------------------- |
| Default                                | A sidebar that is always shown on desktop                      |
| `sidebarCollapsible` on `AppShellRoot` | A sidebar people can hide to give the main area the full width |

```tsx
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
import type { ReactNode } from "react";
import {
  AppShellMainSlot,
  AppShellRailSlot,
  AppShellRoot,
  AppShellSidebarSlot,
  AppShellSidebarTrigger,
  ResizableGroup,
  ResizableHandle,
  ResizablePanel,
} from "@botiverse/raft-ui";
import { cn } from "@botiverse/raft-ui/cn";

function ShellRegion({ children, className }: { children: ReactNode; className: string }) {
  return (
    <div
      className={cn(
        "flex size-full min-h-0  min-w-0 items-center justify-center px-2 text-center font-mono text-xs font-semibold uppercase tracking-wide",
        className,
      )}
    >
      {children}
    </div>
  );
}

function SidebarTrigger() {
  return (
    <AppShellSidebarTrigger>
      <PanelLeftClose className="group-data-[state=collapsed]/app-shell-sidebar-trigger:hidden" />
      <PanelLeftOpen className="hidden group-data-[state=collapsed]/app-shell-sidebar-trigger:block" />
    </AppShellSidebarTrigger>
  );
}

export function AppShellCollapsibleSidebarComposition() {
  return (
    <div className="w-full overflow-x-auto">
      <AppShellRoot sidebarCollapsible className="h-[420px] min-w-[860px]">
        <AppShellRailSlot className="flex md:flex">
          <ShellRegion className="w-16 bg-brand-300 text-brand-950">Rail</ShellRegion>
        </AppShellRailSlot>

        <ResizableGroup orientation="horizontal" className="h-full min-w-0 flex-1">
          <ResizablePanel
            id="app-shell-collapsible-sidebar"
            defaultSize={240}
            minSize={200}
            maxSize={400}
            groupResizeBehavior="preserve-pixel-size"
            className="relative min-w-0"
          >
            <AppShellSidebarSlot className="relative flex h-full w-auto min-w-0 flex-1">
              <ShellRegion className="bg-attention-200 text-attention-950">Sidebar</ShellRegion>
            </AppShellSidebarSlot>
          </ResizablePanel>

          <ResizableHandle />

          <ResizablePanel
            id="app-shell-collapsible-main"
            minSize={360}
            className="relative overflow-visible!"
          >
            <AppShellMainSlot className="flex h-full min-w-0 flex-1">
              <div className="flex size-full  flex-col bg-brand-100 text-brand-950">
                <header className="flex h-12 shrink-0 items-center gap-3 border-b border-line-strong px-3">
                  <SidebarTrigger />
                  <span className="font-mono text-xs font-semibold uppercase tracking-wide">
                    Main
                  </span>
                </header>
                <ShellRegion className="flex-1">
                  Click the control to collapse the sidebar
                </ShellRegion>
              </div>
            </AppShellMainSlot>
          </ResizablePanel>
        </ResizableGroup>
      </AppShellRoot>
    </div>
  );
}
```

In the rail, the trigger is the first item of the footer, and its tooltip shows the shortcut:

```tsx
import { CircleHelp, MessageSquare, PanelLeftClose, PanelLeftOpen, Settings } from "lucide-react";
import { useState } from "react";
import type { ReactNode } from "react";
import {
  AppRailFooter,
  AppRailItem,
  AppRailItemIcon,
  AppRailItemLabel,
  AppRailNav,
  AppRailRoot,
  AppShellMainSlot,
  AppShellRailSlot,
  AppShellRoot,
  AppShellSidebarSlot,
  AppShellSidebarTrigger,
  Kbd,
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@botiverse/raft-ui";

function Region({ children, className }: { children: ReactNode; className: string }) {
  return (
    <div
      className={`flex size-full min-h-0 min-w-0 items-center justify-center px-2 text-center font-mono text-xs font-semibold uppercase tracking-wide ${className}`}
    >
      {children}
    </div>
  );
}

function RailItem({
  label,
  icon,
  value,
  view,
  onSelect,
}: {
  label: string;
  icon: ReactNode;
  value: string;
  view: string;
  onSelect: (value: string) => void;
}) {
  return (
    <Tooltip>
      <TooltipTrigger
        render={
          <AppRailItem
            selected={view === value}
            render={<button type="button" onClick={() => onSelect(value)} />}
          />
        }
      >
        <AppRailItemIcon>{icon}</AppRailItemIcon>
        <AppRailItemLabel>{label}</AppRailItemLabel>
      </TooltipTrigger>
      <TooltipContent side="right">{label}</TooltipContent>
    </Tooltip>
  );
}

// The toggle is the first item of the rail's footer. The shell names it
// ("Collapse Sidebar" / "Expand Sidebar"); the tooltip repeats that name and
// shows the shortcut, so the native title is turned off.
function SidebarToggle({ open }: { open: boolean }) {
  const label = open ? "Collapse Sidebar" : "Expand Sidebar";
  return (
    <Tooltip>
      <AppShellSidebarTrigger
        render={<TooltipTrigger render={<AppRailItem />} />}
        title={undefined}
        aria-keyshortcuts="Meta+\"
      >
        <AppRailItemIcon>
          {open ? <PanelLeftClose aria-hidden /> : <PanelLeftOpen aria-hidden />}
        </AppRailItemIcon>
      </AppShellSidebarTrigger>
      <TooltipContent side="right" className="flex items-center gap-2">
        {label}
        <Kbd aria-label="Command backslash">⌘\</Kbd>
      </TooltipContent>
    </Tooltip>
  );
}

export function AppShellRailTriggerComposition() {
  // The app keeps this value per device, so the sidebar comes back the way it was left.
  const [open, setOpen] = useState(true);
  const [view, setView] = useState("chat");

  return (
    <div className="w-full overflow-x-auto">
      <AppShellRoot
        sidebarCollapsible
        sidebarOpen={open}
        onSidebarOpenChange={setOpen}
        className="h-[420px] min-w-[860px]"
      >
        <AppShellRailSlot className="flex md:flex">
          <AppRailRoot>
            <TooltipProvider delay={650}>
              <AppRailNav>
                <RailItem
                  label="Chat"
                  value="chat"
                  view={view}
                  onSelect={setView}
                  icon={<MessageSquare />}
                />
              </AppRailNav>
              <AppRailFooter>
                <SidebarToggle open={open} />
                <RailItem
                  label="Help"
                  value="help"
                  view={view}
                  onSelect={setView}
                  icon={<CircleHelp />}
                />
                <RailItem
                  label="Settings"
                  value="settings"
                  view={view}
                  onSelect={setView}
                  icon={<Settings />}
                />
              </AppRailFooter>
            </TooltipProvider>
          </AppRailRoot>
        </AppShellRailSlot>

        <AppShellSidebarSlot className="relative flex w-60">
          <Region className="bg-attention-200 text-attention-950">Sidebar</Region>
        </AppShellSidebarSlot>

        <div className="relative flex min-w-0 flex-1">
          <AppShellMainSlot className="flex h-full min-w-0 flex-1">
            <Region className="bg-brand-100 text-brand-950">Main</Region>
          </AppShellMainSlot>
        </div>
      </AppShellRoot>
    </div>
  );
}
```

## Behavior

- Below 768px the rail, sidebar and panel slots are hidden. The main slot shows the page that is open, whether that is a list, a conversation or a thread; the app's routes decide which ([Responsive layout](https://raft-design.botiverse.build/patterns/responsive.md)).
- Put `MobileNav` in the root after the main slot, shown only below 768px, so it sits at the bottom of the column.

```tsx
export function AppShellMobileRootComposition() {

  return (
    <AppShellRoot className="h-dvh w-full">
      <AppShellMainSlot>
        <div className="flex size-full min-h-0  items-center justify-center bg-attention-200 px-4 text-center font-mono text-xs font-semibold uppercase tracking-wide text-attention-950">
          Sidebar · root view
        </div>
      </AppShellMainSlot>
      <MobileNavRoot className="order-last md:hidden" aria-label="Primary navigation">
        {/* ... */}
      </MobileNavRoot>
    </AppShellRoot>
  );
}
```

- The root sets the header height for the regions inside it: 56px, and 48px in a window shorter than 600px ([Spacing](https://raft-design.botiverse.build/foundations/spacing.md#heights)).
- The root clips what overflows it instead of scrolling, so moving focus never shifts the whole app sideways.
- On a phone the root pads its top for the status bar's safe area.
- Collapsing the sidebar slides the main area over it with `ease-slide` while the sidebar fades out; expanding reverses it. With reduced motion, both happen at once ([Motion](https://raft-design.botiverse.build/foundations/motion.md#usage)).
- The main area rises over the sidebar (z-index 2, one above the expanded sidebar, with `data-sliding` on the main slot) only while it slides. At rest it has no z-index, so it never covers the app's own fixed layers, such as a bar at z-40. The expanded sidebar stacks just above the main area, so a resize handle that pokes past its edge stays grabbable.
- The open state is the app's to keep: pass `sidebarOpen` and `onSidebarOpenChange`, and remember the value per device, so the sidebar comes back the way it was left.
- The shell measures the sidebar's width, so a collapsible sidebar can also be resizable.

## Accessibility

- The slots render landmarks: `main` for the main area and `aside` for the others. Name the navigation inside them: `aria-label` on the rail's `nav` and on `MobileNavRoot`.
- `AppShellSidebarTrigger` is a disclosure button: `aria-expanded` says whether the sidebar is shown, and `aria-controls` names the sidebar slot (the slot's own `id`, or one the shell gives it). It shows and hides a region, so it is not a toggle button with `aria-pressed`. The shell labels it "Collapse Sidebar" or "Expand Sidebar", with the same text as a native `title`; when you wrap it in a [Tooltip](https://raft-design.botiverse.build/components/tooltip.md), pass `title={undefined}` and repeat the label in the tooltip. The built-in label is English: to translate it, pass your own `aria-label` (it overrides the shell's) and use the same words in the tooltip. Pass only the icon, hidden from assistive technology. It renders nothing when the root isn't `sidebarCollapsible`, and it is hidden below 768px.
- A collapsed sidebar is `inert`: its controls leave the tab order and the accessibility tree, and it ignores the pointer. A shortcut that collapses the sidebar while focus is inside it moves focus to the trigger first, so focus is never lost.

## Rules

- Build every app screen on AppShell. Don't rebuild the frame from `div`s, because the slots carry the breakpoints, the landmarks and the header height.
- Don't put a background, a border, a width or a shadow on a slot. Put them on the component inside: a [Panel](https://raft-design.botiverse.build/components/panel.md) with `edge="attached"` draws the one line between it and the region before it ([Page structure](https://raft-design.botiverse.build/patterns/page-structure.md#one-line-between-two-regions)).
- Give a screen one `AppShellMainSlot`.
- Set `overlay` on `AppShellMainSlot` only when an `AppShellOverlaySlot` is inside it.

## API

### AppShellMainSlot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `overlay` | `boolean` | `false` |  |
| `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.

### AppShellOverlaySlot

| 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.

### AppShellPanelSlot

| 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.

### AppShellRailSlot

| 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.

### AppShellRoot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultSidebarOpen` | `boolean` | `true` |  |
| `onSidebarOpenChange` | `(open: boolean) => void` |  |  |
| `sidebarCollapsible` | `boolean` | `false` |  |
| `sidebarOpen` | `boolean` |  |  |
| `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.

### AppShellSidebarSlot

| 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.

### AppShellSidebarTrigger

| 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.

## Changes

- 0.4.0 (Changed): **AppShell no longer draws column dividers.** `AppShellRailSlot` and `AppShellSidebarSlot` dropped their border classes in both themes. The rail|sidebar divider now lives on `AppRailRoot` (brutal); seam borders the shell used to own (e.g. sidebar|main with an inset main panel) must be added by the caller. ([e219fd8](https://github.com/botiverse/rui/commit/e219fd8))
- 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 (Fixed): Fixed conditional-hook rendering in `AppShellSidebarTrigger` and `LegacyTaskPanelRoot`, and corrected sortable Composer drag-overlay mapping.
