Skip to content
Dethink Components

ComponentsSidebarShell

SidebarShell

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

On this page

Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.

View registry files
  • React DOM (react-dom) · via shared Dethink components

    Portals for rendering overlay content outside the normal component tree.

Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.

Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.

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.

Make it your workspace

Place navigation on either edge. Try a collapsible work panel inside the shell with activity and notes, switch to a simple bottom status bar, or leave it out. Try RTL and a header-free layout too.

Skip to main content
Workspace overview
Workbench
  • Launch brief

    Ready for your review

  • Indexing documents

    In progress · 62%

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

import { useState, useSyncExternalStore } from "react";
import {
  Button,
  BottomBar,
  BottomBarHeader,
  BottomBarTrigger,
  BottomBarContent,
  type BottomBarSize,
  Sidebar,
  SidebarActivity,
  SidebarContent,
  SidebarFooter,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuLink,
  SidebarMobile,
  SidebarMobileTrigger,
  SidebarShell,
  SidebarShellFooter,
  SidebarShellHeader,
  SidebarShellMain,
  SidebarTrigger,
  type SidebarSide,
  type SidebarActivityItem,
} from "@dethink/components";
import {
  Boxes,
  CircleCheck,
  FileText,
  House,
  Layers,
  Search,
} from "lucide-react";

function WorkspaceNavigation({
  activity,
}: {
  activity: SidebarActivityItem[];
}) {
  return (
    <>
      <SidebarHeader>
        <div className="flex items-center gap-2 px-1 py-2">
          <span className="bg-primary text-primary-foreground flex size-8 shrink-0 items-center justify-center rounded-lg">
            <Layers className="size-4" />
          </span>
          <span className="group-data-[collapsed=true]/sidebar:sr-only">
            <strong className="block text-sm">Studio workspace</strong>
            <span className="text-muted-foreground text-xs">Product team</span>
          </span>
        </div>
      </SidebarHeader>
      <SidebarContent>
        <SidebarMenu>
          {[
            { label: "Overview", icon: House },
            { label: "Projects", icon: Boxes },
            { label: "Documents", icon: FileText },
          ].map(({ label, icon: Icon }, index) => (
            <SidebarMenuItem key={label}>
              <SidebarMenuLink
                href={`#workspace-${label.toLowerCase()}`}
                current={index === 0}
                icon={<Icon aria-hidden="true" />}
              >
                {label}
              </SidebarMenuLink>
            </SidebarMenuItem>
          ))}
        </SidebarMenu>
        <SidebarActivity
          className="mt-4"
          label="Background work"
          items={activity}
        />
      </SidebarContent>
      <SidebarFooter>
        <p className="text-muted-foreground px-2 text-xs group-data-[collapsed=true]/sidebar:sr-only">
          Everything in one place.
        </p>
      </SidebarFooter>
    </>
  );
}

const subscribeHydration = () => () => {};

export function SidebarShellLayoutOptions() {
  const hydrated = useSyncExternalStore(
    subscribeHydration,
    () => true,
    () => false,
  );
  const [side, setSide] = useState<SidebarSide>("left");
  const [footer, setFooter] = useState<
    "none" | "content" | "shell" | "panel" | "panel-shell"
  >("panel");
  const [panelSize, setPanelSize] = useState<BottomBarSize>("sm");
  const [panelOpen, setPanelOpen] = useState(true);
  const [panelView, setPanelView] = useState<"activity" | "notes">("activity");
  const [notes, setNotes] = useState("");
  const [header, setHeader] = useState(true);
  const [chrome, setChrome] = useState<"plain" | "workbench">("workbench");
  const [dir, setDir] = useState<"ltr" | "rtl">("ltr");
  const [saved, setSaved] = useState(false);
  const [reviewed, setReviewed] = useState(false);
  const activity: SidebarActivityItem[] = [
    {
      id: "brief",
      title: "Launch brief",
      status: reviewed ? "complete" : "attention",
      action: reviewed
        ? undefined
        : { label: "Approve brief", onAction: () => setReviewed(true) },
    },
    {
      id: "index",
      title: "Indexing documents",
      status: "running",
      progress: 62,
    },
  ];
  const selectClass =
    "border-border bg-background rounded-md border px-2 py-1.5 text-sm";

  return (
    <div className="space-y-4" data-testid="shell-layout-demo">
      <fieldset
        disabled={!hydrated}
        aria-label="Workspace layout"
        className="flex min-w-0 flex-wrap items-end gap-3"
      >
        <label className="grid gap-1 text-xs">
          Navigation side
          <select
            className={selectClass}
            value={side}
            onChange={(e) => setSide(e.target.value as SidebarSide)}
          >
            <option value="left">Left</option>
            <option value="right">Right</option>
          </select>
        </label>
        <label className="grid gap-1 text-xs">
          Bottom bar
          <select
            className={selectClass}
            value={footer}
            onChange={(e) => setFooter(e.target.value as typeof footer)}
          >
            <option value="none">None</option>
            <option value="content">Content width</option>
            <option value="shell">Full shell width</option>
            <option value="panel">Work panel inside shell</option>
            <option value="panel-shell">Work panel across shell</option>
          </select>
        </label>
        {(footer === "panel" || footer === "panel-shell") && (
          <label className="grid gap-1 text-xs">
            Panel size
            <select
              className={selectClass}
              value={panelSize}
              onChange={(event) =>
                setPanelSize(event.target.value as BottomBarSize)
              }
            >
              <option value="sm">Small</option>
              <option value="md">Medium</option>
              <option value="lg">Large</option>
            </select>
          </label>
        )}
        <label className="grid gap-1 text-xs">
          Appearance
          <select
            className={selectClass}
            value={chrome}
            onChange={(e) => setChrome(e.target.value as typeof chrome)}
          >
            <option value="workbench">Workbench</option>
            <option value="plain">Plain</option>
          </select>
        </label>
        <label className="grid gap-1 text-xs">
          Direction
          <select
            className={selectClass}
            value={dir}
            onChange={(e) => setDir(e.target.value as typeof dir)}
          >
            <option value="ltr">LTR</option>
            <option value="rtl">RTL</option>
          </select>
        </label>
        <label className="flex min-h-9 items-center gap-2 text-sm">
          <input
            type="checkbox"
            checked={header}
            onChange={(e) => setHeader(e.target.checked)}
          />
          Show header
        </label>
      </fieldset>
      <SidebarShell
        side={side}
        dir={dir}
        chrome={chrome}
        motion="subtle"
        className="border-border h-[34rem] rounded-xl border max-md:gap-x-0"
        aria-label="Flexible workspace"
      >
        <Sidebar aria-label="Workspace navigation" className="hidden md:flex">
          <WorkspaceNavigation activity={activity} />
        </Sidebar>
        {header && (
          <SidebarShellHeader as="div">
            <SidebarTrigger className="hidden md:inline-flex" />
            <SidebarMobileTrigger className="md:hidden" />
            <span className="text-sm font-semibold">Workspace overview</span>
            <span className="text-muted-foreground ms-auto hidden items-center gap-1 text-xs sm:flex">
              <CircleCheck className="size-3.5" />
              All changes saved
            </span>
          </SidebarShellHeader>
        )}
        <SidebarShellMain as="section" aria-label="Workspace content">
          <SidebarMobile label="Workspace navigation">
            <WorkspaceNavigation activity={activity} />
          </SidebarMobile>
          {!header && (
            <div className="mb-4">
              <SidebarTrigger className="hidden md:inline-flex" />
              <SidebarMobileTrigger className="md:hidden" />
            </div>
          )}
          <div className="mx-auto max-w-3xl space-y-6">
            <div>
              <p className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                Your workspace
              </p>
              <h3 className="mt-2 text-2xl font-semibold tracking-tight">
                Make room for your work.
              </h3>
              <p className="text-muted-foreground mt-2 text-sm">
                Keep navigation on either edge, with the tools you need always
                within reach.
              </p>
            </div>
            <label className="border-border flex items-center gap-2 rounded-lg border px-3 py-2">
              <Search
                className="text-muted-foreground size-4"
                aria-hidden="true"
              />
              <input
                aria-label="Search workspace"
                placeholder="Search your workspace…"
                className="min-w-0 flex-1 bg-transparent text-sm outline-none"
              />
            </label>
            <div className="grid gap-3 sm:grid-cols-2">
              {[
                "Website refresh",
                "Customer insights",
                "Design system",
                "Autumn launch",
                "Product research",
                "Team handbook",
              ].map((name, i) => (
                <article
                  key={name}
                  className="border-border rounded-lg border p-4"
                >
                  <div className="bg-muted mb-5 flex size-9 items-center justify-center rounded-lg">
                    <Boxes className="text-muted-foreground size-4" />
                  </div>
                  <h4 className="text-sm font-medium">{name}</h4>
                  <p className="text-muted-foreground mt-1 text-xs">
                    {i + 2} people · Updated today
                  </p>
                  <Button
                    className="mt-4"
                    size="sm"
                    variant="ghost"
                    onClick={() => setSaved(false)}
                  >
                    View project
                  </Button>
                </article>
              ))}
            </div>
            <p className="text-muted-foreground text-xs">
              You’re all caught up.
            </p>
          </div>
        </SidebarShellMain>
        {(footer === "panel" || footer === "panel-shell") && (
          <BottomBar
            span={footer === "panel-shell" ? "shell" : "content"}
            size={panelSize}
            open={panelOpen}
            onOpenChange={setPanelOpen}
          >
            <BottomBarHeader className="gap-1">
              <span className="text-foreground me-2 text-xs font-semibold">
                Workbench
              </span>
              <div
                role="group"
                aria-label="Work panel view"
                className="flex gap-1"
              >
                {(["activity", "notes"] as const).map((view) => (
                  <button
                    key={view}
                    type="button"
                    aria-pressed={panelView === view}
                    className="hover:bg-muted aria-pressed:bg-muted aria-pressed:text-foreground focus-visible:outline-ring rounded-md px-2 py-1 text-xs capitalize focus-visible:outline-2"
                    onClick={() => {
                      setPanelView(view);
                      setPanelOpen(true);
                    }}
                  >
                    {view === "activity" ? "Activity" : "Notes"}
                  </button>
                ))}
              </div>
              <BottomBarTrigger
                className="ms-auto"
                expandLabel="Expand work panel"
                collapseLabel="Collapse work panel"
              />
            </BottomBarHeader>
            <BottomBarContent
              aria-label={
                panelView === "activity"
                  ? "Workbench activity"
                  : "Workbench notes"
              }
            >
              {panelView === "activity" ? (
                <ul className="space-y-2">
                  {activity.map((item) => (
                    <li
                      key={item.id}
                      className="border-border bg-background flex flex-wrap items-center gap-2 rounded-lg border p-3"
                    >
                      <div className="min-w-0 flex-1">
                        <p className="text-foreground text-sm font-medium">
                          {item.title}
                        </p>
                        <p className="mt-1 text-xs">
                          {item.status === "attention"
                            ? "Ready for your review"
                            : item.status === "complete"
                              ? "Approved"
                              : "In progress · 62%"}
                        </p>
                      </div>
                      {item.action && (
                        <Button
                          size="sm"
                          variant="outline"
                          onClick={() => setReviewed(true)}
                        >
                          Approve brief
                        </Button>
                      )}
                    </li>
                  ))}
                </ul>
              ) : (
                <label className="grid gap-2 text-xs">
                  Workspace notes
                  <textarea
                    value={notes}
                    onChange={(event) => setNotes(event.target.value)}
                    placeholder="Keep a note beside your work…"
                    className="border-border bg-background text-foreground focus-visible:outline-ring min-h-24 w-full resize-y rounded-md border p-2 text-sm focus-visible:outline-2"
                  />
                </label>
              )}
            </BottomBarContent>
          </BottomBar>
        )}
        {footer !== "none" &&
          footer !== "panel" &&
          footer !== "panel-shell" && (
            <SidebarShellFooter as="div" span={footer}>
              <span className="flex items-center gap-2">
                <span className="bg-success size-1.5 rounded-full" />
                <span>Workspace synced</span>
              </span>
              <div className="ms-auto flex flex-wrap items-center gap-2">
                <span role="status" className="text-xs">
                  {saved ? "Draft saved" : "Ready when you are"}
                </span>
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => setSaved(true)}
                >
                  Save draft
                </Button>
              </div>
            </SidebarShellFooter>
          )}
      </SidebarShell>
    </div>
  );
}

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,
  SidebarShell,
  SidebarShellFooter,
  SidebarShellHeader,
  SidebarTrigger,
  SidebarShellMain,
} from "@dethink/components";
import {
  Bell,
  FolderKanban,
  LayoutDashboard,
  LifeBuoy,
  Plus,
  Users,
} from "lucide-react";

export function SidebarShellBasic() {
  return (
    <SidebarShell
      motion="subtle"
      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>
      </Sidebar>
      <SidebarShellHeader>
        <SidebarTrigger />
        <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,
  SidebarShell,
  SidebarShellHeader,
  SidebarTrigger,
  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>
      </Sidebar>
      <SidebarShellHeader>
        <SidebarTrigger />
        <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 single header trigger 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,
  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
      motion="subtle"
      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>
      </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,
  SidebarShell,
  SidebarShellFooter,
  SidebarShellHeader,
  SidebarTrigger,
  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="subtle"
      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>
      </Sidebar>
      <SidebarShellHeader>
        <SidebarTrigger />
        <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 ml-auto shrink-0 text-xs">
          us-east · 840ms p95
        </span>
      </SidebarShellFooter>
    </SidebarShell>
  );
}

The side prop always names a physical edge, including in RTL layouts. Set it on the shell so navigation, rails, and mobile controls agree.

SidebarFooter belongs inside navigation. SidebarShellFooter is the application’s bottom bar: its default span is content; span="shell" places it beneath navigation and the work area. Omit it to remove the bar entirely. Header and footer are independently optional.

For an embedded work panel, compose BottomBar, BottomBarHeader, BottomBarTrigger and BottomBarContent directly inside SidebarShell. Use span="content" or span="shell" and choose a size or custom height. The example keeps Activity and Notes close to the workspace; collapsing the panel gives that space back to the main content. Panel state belongs to your application.

Pass regions directly or inside fragments. Wrap an extracted navigation component in SidebarShellNavigation. The main region scrolls independently; long navigation scrolls in SidebarContent. Keep bar content concise and allow actions to wrap.

The skip link comes first. Left-side navigation precedes the content frame in DOM order; right-side navigation follows it. A full-width bottom bar comes last. On small screens, hide desktop navigation and compose SidebarMobile with SidebarMobileTrigger, as shown above; the shell does not choose your breakpoint.

Existing compositions need no migration. Footer span is additive. Use as="div" when a parent already owns the footer landmark, and provide distinct navigation labels when your page has multiple navigation regions.

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 main content scroll container: header and footer stay pinned while this canvas scrolls with overscroll containment.as: "main", tabIndex: 0
SidebarShellFooterfooter attributes + { as: "footer" | "div", span: "content" | "shell" }Optional bottom bar. span="content" stays below the work stage; span="shell" stretches below both navigation and content. Omit to remove it without reserved space. Content can wrap; renders a contentinfo landmark by default.as: "footer", span: "content"
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