AppShell

Layout-only foundations for rail, sidebar, main content, and optional panels.

Preview#

Main
Click the control to collapse the sidebar
Controls
import {
  AppShellMainSlot,
  AppShellOverlaySlot,
  AppShellPanelSlot,
  AppShellRailSlot,
  AppShellRoot,
  AppShellSidebarSlot,
} from "raft-ui";

<AppShellRoot>
  <AppShellRailSlot>...</AppShellRailSlot>
  <AppShellSidebarSlot>...</AppShellSidebarSlot>
  <AppShellMainSlot overlay>
    ...
    <AppShellOverlaySlot>...</AppShellOverlaySlot>
  </AppShellMainSlot>
  <AppShellPanelSlot>...</AppShellPanelSlot>
</AppShellRoot>

Responsive#

AppShell hides the rail and sidebar and switches to a column below md. Keep the active page in AppShellMainSlot and add mobile navigation with md:hidden.

apps/doc/src/modules/app-shell/compositions/responsive.composition.tsx

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>
  );
}

Set sidebarCollapsible on the root and render AppShellSidebarTrigger in the main panel. The sidebar remains caller-owned; the shell only measures its width and coordinates the collapse transition. For a resizable desktop shell, keep the sidebar and main slots in sibling relative panels as shown below.

Main
Click the control to collapse the sidebar

apps/doc/src/modules/app-shell/compositions/collapsible-sidebar.composition.tsx

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={252}
            minSize={180}
            maxSize={368}
            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>
  );
}

API#WIP

sidebarCollapsibledefault: false
boolean
When true, coordinates sidebar visibility and measures sidebar width for the main-panel transition.
sidebarOpen
boolean
Controlled sidebar visibility when sidebarCollapsible is enabled.
defaultSidebarOpendefault: true
boolean
Initial sidebar visibility for uncontrolled usage.
onSidebarOpenChange
(open: boolean) => void
Called when a collapsible sidebar opens or closes.
...props
useRender.ComponentProps<"div">
Owns the responsive row-to-column layout and top safe-area inset.
...props
useRender.ComponentProps<"aside">
Desktop rail column by default. Mount a separate instance with className for mobile bottom navigation; visibility stays caller-owned.
...props
useRender.ComponentProps<"aside">
Desktop sidebar column by default. When the root is collapsible, its measured width drives the main-panel transition. Override with className when the caller mounts it below md.
overlaydefault: false
boolean
Provides the positioning context for an AppShellOverlaySlot rendered inside the main region.
...props
useRender.ComponentProps<"main">
Flexible primary content region. In a collapsible desktop layout, mount it inside a relative main panel so it can expand over the sidebar.
...props
useRender.ComponentProps<"div">
Layout-only overlay region for temporarily covering the primary content without changing the column layout. Its child owns the surface and edges.
...props
useRender.ComponentProps<"aside">
Optional secondary column. Place it in a caller-owned ResizablePanel when the user can adjust the split; the caller owns its width and surface.
...props
PanelAction props
Toggles a collapsible root and returns null otherwise. Provide the caller-owned icon as children; it supplies accessible collapse and expand labels.