Sidebar
App-shell sidebar with collapsible width, context-driven open state, and composable Header/Content/Footer/Item parts. Provider-based so any descendant can toggle it.
Click the toggle to collapse or expand the sidebar.
Installation
pnpm dlx @hex-core/cli add sidebarCollapsible with icon-only labels
Wrap visible labels in a span that hides when the provider is closed; icons stay visible so the rail remains scannable. The trigger flips the same context, so any descendant can collapse the shell.
"use client";
import {
Sidebar,
SidebarContent,
SidebarHeader,
SidebarItem,
SidebarProvider,
SidebarTrigger,
useSidebar,
} from "@/components/ui";
// SidebarProvider's root <div> already carries data-state="open"|"closed",
// so this could also be expressed CSS-only via a tagged Tailwind group on
// that root (e.g. group/sidebar + group-data-[state=closed]/sidebar:hidden
// on the label span) if SidebarProvider exposes a CSS group class. The
// hook form is kept here because it works against the shipped surface
// without requiring a parent className that may not exist in your build.
function NavRow({ label, children }: { label: string; children: React.ReactNode }) {
const { open } = useSidebar();
return (
<SidebarItem aria-label={open ? undefined : label} title={label}>
{children}
{open ? <span>{label}</span> : null}
</SidebarItem>
);
}
export function Example() {
return (
<SidebarProvider defaultOpen={false}>
<Sidebar>
<SidebarHeader>
<SidebarTrigger />
</SidebarHeader>
<SidebarContent>
<NavRow label="Dashboard">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 shrink-0"
aria-hidden="true"
>
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
</NavRow>
<NavRow label="Projects">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 shrink-0"
aria-hidden="true"
>
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" />
</svg>
</NavRow>
</SidebarContent>
</Sidebar>
<main className="flex-1 p-4">
<h1>App shell</h1>
</main>
</SidebarProvider>
);
}Variant values
sideWhich edge the sidebar docks against| Value | Description |
|---|---|
leftdefault | Docks to the left edge (default) |
right | Docks to the right edge |
stateWidth state (derived from SidebarProvider open value)| Value | Description |
|---|---|
opendefault | Sidebar expanded at full width |
closed | Sidebar collapsed to zero width |
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state — read from SidebarProvider |
defaultOpen | boolean | true | Initial open state (uncontrolled) |
onOpenChange | function | — | Callback when open state flips: (open: boolean) => void |
side | string | left | Which edge the sidebar sits on: 'left' | 'right' |
AI Guidance
When to use
Use for persistent app-shell navigation: admin dashboards, document editors, SaaS sidebars. The Provider pattern lets any descendant component toggle the sidebar (e.g. a topbar button on mobile).
When not to use
Don't use for mobile-first UX (use Sheet — sidebar collapses to zero-width but Sheet gives a native drawer feel). Don't use for marketing sites (no shell). Don't use for contextual menus (use DropdownMenu or NavigationMenu).
Common mistakes
- Rendering Sidebar outside SidebarProvider — useSidebar throws
- Forgetting that SidebarProvider is a flex container — main content must be its direct sibling
- Using the wrong ordering for side='right' — SidebarProvider handles this via order-last
- Overriding the width variant manually instead of toggling open state
Accessibility
Sidebar is an <aside> landmark (not a modal — no focus trap). When collapsed, the aside sets inert + aria-hidden so its children are removed from the tab order and the accessibility tree. SidebarTrigger exposes aria-expanded and a rotating aria-label (suppressed when asChild so the consumer's visible label/aria-label wins). SidebarItem uses aria-current='page' when active. Focus rings use the ring token.
Related components
Token budget: 900
Verified against @hex-core/components@1.12.0