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 className="in-data-[theme=brutal]:border-r-2 in-data-[theme=brutal]:border-black">
        <nav className="w-16 [[data-theme=elegant]_&]:w-14" aria-label="Workspace navigation">
          Rail
        </nav>
      </AppShellRailSlot>

      <AppShellSidebarSlot className="in-data-[theme=brutal]:border-r-2 in-data-[theme=brutal]:border-black">
        <div className="w-60 in-data-[theme=elegant]:w-[252px]">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>
  );
}

Collapsible sidebar#

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 in-data-[theme=brutal]:border-r-2 in-data-[theme=brutal]:border-black">
          <ShellRegion className="w-16 bg-primary-300 text-primary-950 [[data-theme=elegant]_&]:w-14">
            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 in-data-[theme=brutal]:border-r-2 in-data-[theme=brutal]:border-black">
              <ShellRegion className="bg-accent-200 text-accent-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-primary-100 text-primary-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

PropTypeDefaultDescription
sidebarCollapsiblebooleanfalseWhen true, coordinates sidebar visibility and measures sidebar width for the main-panel transition.
sidebarOpenbooleanControlled sidebar visibility when sidebarCollapsible is enabled.
defaultSidebarOpenbooleantrueInitial sidebar visibility for uncontrolled usage.
onSidebarOpenChange(open: boolean) => voidCalled when a collapsible sidebar opens or closes.
...propsuseRender.ComponentProps<"div">Owns the responsive row-to-column layout and top safe-area inset.
PropTypeDefaultDescription
...propsuseRender.ComponentProps<"aside">Desktop rail column by default. Mount a separate instance with className for mobile bottom navigation; visibility stays caller-owned.
PropTypeDefaultDescription
...propsuseRender.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.
PropTypeDefaultDescription
overlaybooleanfalseProvides the positioning context for an AppShellOverlaySlot rendered inside the main region.
...propsuseRender.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.
PropTypeDefaultDescription
...propsuseRender.ComponentProps<"div">Layout-only overlay region for temporarily covering the primary content without changing the column layout. Its child owns the surface and edges.
PropTypeDefaultDescription
...propsuseRender.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.
PropTypeDefaultDescription
...propsPanelAction propsToggles a collapsible root and returns null otherwise. Provide the caller-owned icon as children; it supplies accessible collapse and expand labels.