Skip to content
Dethink Components

ComponentsSidebarShell

SidebarShell

Arrange a sidebar, header, main content, and footer into an app layout.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import {
  SidebarShell,
  SidebarShellHeader,
  SidebarShellMain,
  SidebarShellFooter,
} from "@dethink/components";

Try the examples, then open the code to use them in your app. The shell fills the viewport by default; these previews bound it with a height utility.

Workbench shell

The default bounded-workbench chrome: navigation and the work stage float as related panels on a calm inset canvas, with a pinned command deck, scrolling main landmark, and status footer.

Skip to main content
Overview
Everything shipping this week
Active projects
24
Open reviews
7
Deploys today
132
The main region is the only scroll container. Header and footer stay pinned while this canvas scrolls.
All systems operationalv2.4.0
Show sourceexamples/sidebar-shell/basic.tsx
examples/sidebar-shell/basic.tsx
"use client";

import {
  Badge,
  Button,
  IconButton,
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuLink,
  SidebarRail,
  SidebarShell,
  SidebarShellFooter,
  SidebarShellHeader,
  SidebarShellMain,
} from "@dethink/components";
import {
  Bell,
  FolderKanban,
  LayoutDashboard,
  LifeBuoy,
  Plus,
  Users,
} from "lucide-react";

export function SidebarShellBasic() {
  return (
    <SidebarShell
      aria-label="Workbench shell demo"
      className="border-border h-[30rem] rounded-xl border"
    >
      <Sidebar aria-label="Product navigation">
        <SidebarHeader>
          <div className="flex min-w-0 items-center gap-2">
            <span
              aria-hidden="true"
              className="bg-primary text-primary-foreground flex size-8 shrink-0 items-center justify-center rounded-md text-sm font-semibold"
            >
              D
            </span>
            <div className="min-w-0 group-data-[collapsed=true]:sr-only">
              <div className="text-foreground truncate text-sm font-semibold">
                Dethink Cloud
              </div>
              <div className="text-muted-foreground truncate text-xs">
                Operations
              </div>
            </div>
          </div>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Workspace</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuLink
                    current
                    href="#overview"
                    icon={<LayoutDashboard aria-hidden="true" />}
                  >
                    Overview
                  </SidebarMenuLink>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuLink
                    badge="8"
                    href="#projects"
                    icon={<FolderKanban aria-hidden="true" />}
                  >
                    Projects
                  </SidebarMenuLink>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuLink
                    href="#customers"
                    icon={<Users aria-hidden="true" />}
                  >
                    Customers
                  </SidebarMenuLink>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
          <SidebarGroup>
            <SidebarGroupLabel>Support</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuLink
                    href="#help"
                    icon={<LifeBuoy aria-hidden="true" />}
                  >
                    Help center
                  </SidebarMenuLink>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarRail />
      </Sidebar>
      <SidebarShellHeader>
        <div className="min-w-0">
          <div className="text-foreground truncate text-sm font-semibold">
            Overview
          </div>
          <div className="text-muted-foreground truncate text-xs">
            Everything shipping this week
          </div>
        </div>
        <div className="ml-auto flex items-center gap-2">
          <IconButton aria-label="Notifications" size="sm" variant="ghost">
            <Bell aria-hidden="true" />
          </IconButton>
          <Button size="sm" leftIcon={<Plus aria-hidden="true" />}>
            New project
          </Button>
        </div>
      </SidebarShellHeader>
      <SidebarShellMain>
        <div className="grid gap-4 sm:grid-cols-3">
          {[
            { label: "Active projects", value: "24" },
            { label: "Open reviews", value: "7" },
            { label: "Deploys today", value: "132" },
          ].map((stat) => (
            <div
              key={stat.label}
              className="border-border bg-background rounded-lg border p-4"
            >
              <div className="text-muted-foreground text-xs">{stat.label}</div>
              <div className="text-foreground mt-1 text-2xl font-semibold">
                {stat.value}
              </div>
            </div>
          ))}
        </div>
        <div className="border-border bg-muted/30 text-muted-foreground mt-4 rounded-lg border p-4 text-sm">
          The main region is the only scroll container. Header and footer stay
          pinned while this canvas scrolls.
        </div>
      </SidebarShellMain>
      <SidebarShellFooter>
        <span className="truncate">All systems operational</span>
        <Badge className="ml-auto" size="xs" tone="neutral" variant="soft">
          v2.4.0
        </Badge>
      </SidebarShellFooter>
    </SidebarShell>
  );
}

Plain chrome

Neutral edge-to-edge geometry for docs sites and settings areas. The footer region is optional, and the subtle motion preset keeps movement nearly imperceptible.

Skip to main content
Writing pages

Plain chrome removes the workbench framing: no inset gap, no floating panels, just neutral edge-to-edge geometry.

Use it for documentation sites, settings areas, or anywhere the shell should disappear behind the content. The footer region is optional — this example simply leaves it out.

Show sourceexamples/sidebar-shell/plain.tsx
examples/sidebar-shell/plain.tsx
"use client";

import {
  Sidebar,
  SidebarContent,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuLink,
  SidebarRail,
  SidebarShell,
  SidebarShellHeader,
  SidebarShellMain,
} from "@dethink/components";
import { BookOpen, FileText, Home } from "lucide-react";

export function SidebarShellPlain() {
  return (
    <SidebarShell
      aria-label="Plain shell demo"
      chrome="plain"
      motion="subtle"
      className="border-border h-[24rem] rounded-lg border"
    >
      <Sidebar aria-label="Docs navigation">
        <SidebarHeader>
          <div className="flex min-w-0 items-center gap-2 group-data-[collapsed=true]:justify-center">
            <BookOpen
              aria-hidden="true"
              className="text-primary size-4 shrink-0"
            />
            <span className="text-foreground truncate text-sm font-semibold group-data-[collapsed=true]:sr-only">
              Field Guide
            </span>
          </div>
        </SidebarHeader>
        <SidebarContent>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuLink href="#home" icon={<Home aria-hidden="true" />}>
                Getting started
              </SidebarMenuLink>
            </SidebarMenuItem>
            <SidebarMenuItem>
              <SidebarMenuLink
                current
                href="#writing"
                icon={<FileText aria-hidden="true" />}
              >
                Writing pages
              </SidebarMenuLink>
            </SidebarMenuItem>
            <SidebarMenuItem>
              <SidebarMenuLink
                href="#reference"
                icon={<BookOpen aria-hidden="true" />}
              >
                Reference
              </SidebarMenuLink>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarContent>
        <SidebarRail />
      </Sidebar>
      <SidebarShellHeader>
        <div className="text-foreground truncate text-sm font-semibold">
          Writing pages
        </div>
      </SidebarShellHeader>
      <SidebarShellMain>
        <div className="max-w-prose space-y-3 text-sm leading-6">
          <p className="text-foreground">
            Plain chrome removes the workbench framing: no inset gap, no
            floating panels, just neutral edge-to-edge geometry.
          </p>
          <p className="text-muted-foreground">
            Use it for documentation sites, settings areas, or anywhere the
            shell should disappear behind the content. The footer region is
            optional — this example simply leaves it out.
          </p>
        </div>
      </SidebarShellMain>
    </SidebarShell>
  );
}

Controlled collapse

Collapse state lifted into your component. The header trigger, the edge rail, and every region's data attributes stay in agreement, so you can persist the preference or drive it from a server.

Skip to main content
Inbox
Rail
The shell starts on the compact icon rail. Collapse state lives in your component, so you can persist it, sync it with a server preference, or drive it from anywhere — the trigger, the edge rail, and every region stay in agreement.
Navigation is collapsed to the icon rail.
Show sourceexamples/sidebar-shell/controlled.tsx
examples/sidebar-shell/controlled.tsx
"use client";

import { useState } from "react";
import {
  Badge,
  Sidebar,
  SidebarContent,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuLink,
  SidebarRail,
  SidebarShell,
  SidebarShellFooter,
  SidebarShellHeader,
  SidebarShellMain,
  SidebarTrigger,
} from "@dethink/components";
import { Inbox, ListChecks, Timer } from "lucide-react";

export function SidebarShellControlled() {
  const [collapsed, setCollapsed] = useState(true);

  return (
    <SidebarShell
      aria-label="Controlled shell demo"
      collapsed={collapsed}
      onCollapsedChange={setCollapsed}
      className="border-border h-[24rem] rounded-xl border"
    >
      <Sidebar aria-label="Queue navigation">
        <SidebarHeader>
          <div className="flex min-w-0 items-center gap-2">
            <span
              aria-hidden="true"
              className="bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-md text-sm font-semibold"
            >
              T
            </span>
            <span className="text-foreground truncate text-sm font-semibold group-data-[collapsed=true]:sr-only">
              Triage
            </span>
          </div>
        </SidebarHeader>
        <SidebarContent>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuLink
                current
                badge="14"
                href="#inbox"
                icon={<Inbox aria-hidden="true" />}
              >
                Inbox
              </SidebarMenuLink>
            </SidebarMenuItem>
            <SidebarMenuItem>
              <SidebarMenuLink
                href="#assigned"
                icon={<ListChecks aria-hidden="true" />}
              >
                Assigned
              </SidebarMenuLink>
            </SidebarMenuItem>
            <SidebarMenuItem>
              <SidebarMenuLink
                href="#snoozed"
                icon={<Timer aria-hidden="true" />}
              >
                Snoozed
              </SidebarMenuLink>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarContent>
        <SidebarRail />
      </Sidebar>
      <SidebarShellHeader>
        <SidebarTrigger />
        <div className="text-foreground truncate text-sm font-semibold">
          Inbox
        </div>
        <Badge className="ml-auto" size="xs" tone="primary" variant="soft">
          {collapsed ? "Rail" : "Expanded"}
        </Badge>
      </SidebarShellHeader>
      <SidebarShellMain>
        <div className="border-border bg-muted/30 text-muted-foreground rounded-lg border p-4 text-sm leading-6">
          The shell starts on the compact icon rail. Collapse state lives in
          your component, so you can persist it, sync it with a server
          preference, or drive it from anywhere — the trigger, the edge rail,
          and every region stay in agreement.
        </div>
      </SidebarShellMain>
      <SidebarShellFooter>
        <span className="truncate">
          Navigation is {collapsed ? "collapsed to the icon rail" : "expanded"}.
        </span>
      </SidebarShellFooter>
    </SidebarShell>
  );
}

Examples that combine components for common tasks.

AI operations console

An expressive-motion workbench for a live agent fleet: collapsible navigation groups with badges, a command deck with production status and actions, streaming stats and activity in the work stage, and a system-status footer.

Skip to main content
Fleet overviewProduction
Active agents
38
+4 this hour
Eval pass rate
92%
+1.5 pts
P95 latency
840ms
-60ms
Recent activityAuto-refresh
  • Escalation triaged by support-agent2m ago
  • Retry budget reached on billing-sync11m ago
  • New eval suite promoted to production26m ago
All systems operationalus-east · 840ms p95
Show sourceexamples/sidebar-shell/recipe-ops-console.tsx
examples/sidebar-shell/recipe-ops-console.tsx
"use client";

import {
  Badge,
  Button,
  IconButton,
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupTrigger,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuItem,
  SidebarMenuLink,
  SidebarRail,
  SidebarShell,
  SidebarShellFooter,
  SidebarShellHeader,
  SidebarShellMain,
} from "@dethink/components";
import {
  Activity,
  Bell,
  Bot,
  Gauge,
  Radar,
  Settings2,
  Sparkles,
  Workflow,
} from "lucide-react";

const fleetStats = [
  { label: "Active agents", value: "38", delta: "+4 this hour" },
  { label: "Eval pass rate", value: "92%", delta: "+1.5 pts" },
  { label: "P95 latency", value: "840ms", delta: "-60ms" },
];

const fleetEvents = [
  {
    id: "run-4821",
    title: "Escalation triaged by support-agent",
    time: "2m ago",
    tone: "bg-success",
  },
  {
    id: "run-4820",
    title: "Retry budget reached on billing-sync",
    time: "11m ago",
    tone: "bg-warning",
  },
  {
    id: "run-4819",
    title: "New eval suite promoted to production",
    time: "26m ago",
    tone: "bg-info",
  },
];

export function SidebarShellRecipeOpsConsole() {
  return (
    <SidebarShell
      aria-label="AI operations console"
      motion="expressive"
      className="border-border h-[34rem] rounded-xl border"
    >
      <Sidebar aria-label="Console navigation">
        <SidebarHeader>
          <div className="flex min-w-0 items-center gap-2">
            <span
              aria-hidden="true"
              className="from-primary to-primary/70 text-primary-foreground flex size-8 shrink-0 items-center justify-center rounded-md bg-gradient-to-br"
            >
              <Sparkles className="size-4" />
            </span>
            <div className="min-w-0 group-data-[collapsed=true]:sr-only">
              <div className="text-foreground truncate text-sm font-semibold">
                Assist Studio
              </div>
              <div className="text-muted-foreground truncate text-xs">
                Fleet operations
              </div>
            </div>
          </div>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup collapsible defaultOpen>
            <SidebarGroupTrigger>Monitors</SidebarGroupTrigger>
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuLink
                    current
                    badge="Live"
                    description="Streaming run telemetry"
                    href="#fleet"
                    icon={<Radar aria-hidden="true" />}
                  >
                    Fleet overview
                  </SidebarMenuLink>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuLink
                    href="#agents"
                    icon={<Bot aria-hidden="true" />}
                  >
                    Agents
                  </SidebarMenuLink>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuLink
                    badge="3"
                    href="#evaluations"
                    icon={<Activity aria-hidden="true" />}
                  >
                    Evaluations
                  </SidebarMenuLink>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
          <SidebarGroup collapsible defaultOpen={false}>
            <SidebarGroupTrigger>Automation</SidebarGroupTrigger>
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuLink
                    href="#workflows"
                    icon={<Workflow aria-hidden="true" />}
                  >
                    Workflows
                  </SidebarMenuLink>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuLink
                    disabled
                    href="#budgets"
                    icon={<Gauge aria-hidden="true" />}
                  >
                    Cost budgets
                  </SidebarMenuLink>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>
          <div className="flex min-w-0 flex-1 items-center justify-between gap-2">
            <span className="text-muted-foreground truncate text-sm group-data-[collapsed=true]:sr-only">
              Team quota · 64%
            </span>
            <SidebarMenuAction aria-label="Open quota settings">
              <Settings2 aria-hidden="true" />
            </SidebarMenuAction>
          </div>
        </SidebarFooter>
        <SidebarRail />
      </Sidebar>
      <SidebarShellHeader>
        <div className="flex min-w-0 items-center gap-2">
          <span className="text-foreground truncate text-sm font-semibold">
            Fleet overview
          </span>
          <Badge size="xs" tone="success" variant="soft">
            Production
          </Badge>
        </div>
        <div className="ml-auto flex items-center gap-2">
          <IconButton aria-label="Notifications" size="sm" variant="ghost">
            <Bell aria-hidden="true" />
          </IconButton>
          <Button
            size="sm"
            variant="soft"
            leftIcon={<Sparkles aria-hidden="true" />}
          >
            Launch agent
          </Button>
        </div>
      </SidebarShellHeader>
      <SidebarShellMain>
        <div className="grid gap-4 lg:grid-cols-3">
          {fleetStats.map((stat) => (
            <div
              key={stat.label}
              className="border-border bg-background rounded-lg border p-4"
            >
              <div className="text-muted-foreground text-xs">{stat.label}</div>
              <div className="text-foreground mt-1 text-2xl font-semibold">
                {stat.value}
              </div>
              <div className="text-muted-foreground mt-1 text-xs">
                {stat.delta}
              </div>
            </div>
          ))}
        </div>
        <div className="border-border bg-background mt-4 rounded-lg border">
          <div className="border-border flex items-center justify-between border-b px-4 py-3">
            <span className="text-foreground text-sm font-semibold">
              Recent activity
            </span>
            <Badge size="xs" tone="info" variant="outline">
              Auto-refresh
            </Badge>
          </div>
          <ul className="divide-border divide-y">
            {fleetEvents.map((event) => (
              <li
                key={event.id}
                className="flex items-center gap-3 px-4 py-3 text-sm"
              >
                <span
                  aria-hidden="true"
                  className={`size-2 shrink-0 rounded-full ${event.tone}`}
                />
                <span className="text-foreground min-w-0 flex-1 truncate">
                  {event.title}
                </span>
                <span className="text-muted-foreground shrink-0 text-xs">
                  {event.time}
                </span>
              </li>
            ))}
          </ul>
        </div>
      </SidebarShellMain>
      <SidebarShellFooter>
        <span
          aria-hidden="true"
          className="bg-success size-2 shrink-0 rounded-full"
        />
        <span className="truncate">All systems operational</span>
        <span className="text-muted-foreground/70 ml-auto shrink-0 text-xs">
          us-east · 840ms p95
        </span>
      </SidebarShellFooter>
    </SidebarShell>
  );
}

SidebarShell is composable. The root owns shared shell state and wraps SidebarProvider; regions render semantic landmarks around a Sidebar passed as a direct child.

SidebarShell props
PropWhat it doesDefault
SidebarShell{ chrome, side, variant, motion, animate, collapsed, defaultCollapsed, onCollapsedChange, mobileOpen, defaultMobileOpen, onMobileOpenChange, mainId }The shell root. Wraps SidebarProvider, owns collapsed and mobile drawer state (controlled or uncontrolled), renders an automatic skip link, and frames header, main, and footer beside a direct Sidebar child. chrome switches between the bounded workbench treatment and plain edge-to-edge geometry.chrome: "workbench", motion: "standard"
SidebarShellHeaderheader attributes + { as: "header" | "div" }The command deck above the work stage. Renders a banner landmark by default; use as="div" when the page already owns a banner. Compose breadcrumbs, page titles, and actions into it — the shell does not own them.as: "header"
SidebarShellMainmain attributes + { as: "main" | "div" | "section", id, tabIndex }The single main landmark and skip-link target. The only scroll container in the shell: header and footer stay pinned while this canvas scrolls with overscroll containment.as: "main", tabIndex: -1
SidebarShellFooterfooter attributes + { as: "footer" | "div" }Optional status strip pinned below the work stage for system status, version, or legal text. Renders a contentinfo landmark by default.as: "footer"
SidebarShellNavigationdiv attributesOptional wrapper for custom navigation surfaces. Passing a Sidebar directly as a shell child is the common path; this region exists for non-Sidebar navigation that should still receive shell chrome and state attributes.Not set
SidebarShellSkipLinkanchor attributes + { targetId }Focus-visible skip link rendered first in the shell. One is provided automatically; supply your own to change the label or target.targets the shell main region
getSidebarShellMotionConfig / *ClassNames helpers(motion, reducedMotion) => SidebarShellMotionConfig, ({ className }) => stringOpen-code escape hatches: read the resolved motion preset (spring transition, lift, hover and tap scales) or reuse the shell region class recipes on custom elements.Not set