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- Motion for React
(motion)Animations, transitions, and reduced-motion handling.
- React DOM
(react-dom)· via shared Dethink componentsPortals 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.
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.
Show sourceexamples/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.
Show sourceexamples/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.
Show sourceexamples/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.
Show sourceexamples/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.
Show sourceexamples/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.
| Prop | What it does | Default |
|---|---|---|
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 attributes | Optional 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 }) => string | Open-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 |