Sidebar
A collapsible navigation rail that toggles between a labelled 240px column and a 56px icon-only rail. State lives in a SidebarProvider context that persists to localStorage, registers a global Cmd/Ctrl+B toggle, and surfaces row labels as right-side tooltips while collapsed. Composed via slot parts — header, scrollable content, footer, groups, and menu rows.
Import
Sidebar is a registry composite — you own the source after installing:
npx ploxum add sidebarimport {
SidebarProvider,
Sidebar,
SidebarHeader,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarSeparator,
SidebarTrigger,
SidebarRail,
useSidebar,
} from "@/components/sidebar";Depends on @roger-emerson/ploxum-primitives (Button, ScrollArea, Separator, Tooltip, useDensity) and @roger-emerson/ploxum-utils (cn).
Usage
Wrap the rail (and anything that toggles it) in a SidebarProvider. The Sidebar <nav> reads collapse state from context and pins its width via the --sidebar-width CSS var, so the transition stays GPU-cheap. Give it a bounded, relative parent if you use SidebarRail:
<SidebarProvider defaultCollapsed={false}>
<div className="relative flex h-screen">
<Sidebar>
<SidebarHeader>
<SidebarTrigger />
<span className="font-medium lowercase tracking-tight">ploxum</span>
</SidebarHeader>
<SidebarSeparator className="my-0" />
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Command</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton icon={<Eye size={16} />} isActive tooltip="Overview">
Overview
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton icon={<MapPin size={16} />} tooltip="Venue">
Venue
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>{/* account / settings row */}</SidebarFooter>
<SidebarRail />
</Sidebar>
<main className="flex-1">{/* page */}</main>
</div>
</SidebarProvider>Controlled
Pass collapsed + onCollapsedChange to drive the rail from your own state (persistence is skipped when controlled). Render an <a> / router Link as a row with asChild:
const [collapsed, setCollapsed] = useState(false);
<SidebarProvider collapsed={collapsed} onCollapsedChange={setCollapsed}>
<Sidebar>
<SidebarContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild icon={<Shield size={16} />} isActive tooltip="Security">
<a href="/security">Security</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarContent>
</Sidebar>
</SidebarProvider>;Read the live state anywhere below the provider with useSidebar() ({ collapsed, setCollapsed, toggle }).
Props
SidebarProvider
| Prop | Type | Default | Description |
|---|---|---|---|
collapsed | boolean | — | Controlled collapsed state. When set, localStorage persistence is skipped. |
defaultCollapsed | boolean | false | Initial collapsed state when uncontrolled. |
onCollapsedChange | (collapsed: boolean) => void | — | Fires on every collapse change (controlled or not). |
storageKey | string | "ploxum.sidebar" | localStorage key for uncontrolled persistence. |
children | ReactNode | — | The rail plus any consumers of useSidebar(). |
Sidebar
| Prop | Type | Default | Description |
|---|---|---|---|
collapsed | boolean | context / false | Optional override; otherwise reads useSidebar(). Lets the rail render without a provider. |
label | string | "Primary" | aria-label for the <nav> landmark. |
className | string | — | Merged onto the <nav>. |
ref | Ref<HTMLElement> | — | Forwarded to the <nav>. |
Remaining HTMLAttributes<HTMLElement> pass through.
SidebarMenuButton
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | Leading glyph. Always visible — it is the only content when collapsed. |
isActive | boolean | false | Active row: amber-tint background, amber text, and a left accent bar. |
tooltip | ReactNode | string children | Label shown in a right-side Tooltip only while collapsed. Falls back to the text children when those are a plain string. |
asChild | boolean | false | Render the child element (e.g. an <a> / router Link) as the row, merging styling + the icon/label scaffold. |
className | string | — | Merged onto the row. |
ref | Ref<HTMLButtonElement> | — | Forwarded to the row. |
Remaining ButtonHTMLAttributes pass through.
SidebarTrigger / SidebarRail
SidebarTrigger is a ghost icon Button (PanelLeft glyph) that calls toggle(). SidebarRail is the thin hover-revealed edge strip on the rail's trailing border that also toggles collapse on click — give it a relative ancestor so it positions correctly.
Slot wrappers
SidebarHeader, SidebarContent (wraps ScrollArea), SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuItem, and SidebarSeparator are thin token-styled wrappers that forward all native element props and carry data-slot attributes for styling hooks. SidebarGroupLabel renders null while collapsed.
useSidebar()
Returns { collapsed, setCollapsed, toggle }. Throws when used outside a SidebarProvider so the collapse parts never silently no-op.
Design rules
- Width is the only motion. Collapse animates
widthbetween 240px (WIDTH_EXPANDED) and 56px (WIDTH_ICON) via the--sidebar-widthCSS var withtransition-[width] duration-200, gated bymotion-reduce. Don't animate row contents. - Collapsed labels move to tooltips. When collapsed,
SidebarMenuButtonhides its text and surfaces the label in a right-anchoredTooltip;SidebarGroupLabelunmounts. The icon is mandatory — it's the only content left. - Active is shape + colour, never hue alone. The active row pairs an amber tint and amber text with a left accent bar (
--ploxum-color-amber-bright), and setsdata-active="true"for styling hooks. - State is persisted, but the host owns control. Uncontrolled, the provider seeds from and writes back to
localStorageunderstorageKey. Passcollapsedto control it externally — persistence is then skipped and the caller is the source of truth. - Cmd/Ctrl+B is global. The provider registers a window-level keydown toggle. Density-aware: rows are shorter under
compactdensity (useDensity). SidebarNav(package pattern) vsSidebar(registry composite). Reach for the packageSidebarNavpattern when you want a fixed, always-labelled rail; reach for this registrySidebarwhen you need the collapse-to-icon behaviour and want to own the source.
