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.

Toggle the sidebar

Click the toggle to collapse or expand the sidebar.

Installation

pnpm
pnpm dlx @hex-core/cli add sidebar

Collapsible 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.

tsx
"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
Page body
Page body
ValueDescription
leftdefault
Docks to the left edge (default)
right
Docks to the right edge
stateWidth state (derived from SidebarProvider open value)
Page body
Page body
ValueDescription
opendefault
Sidebar expanded at full width
closed
Sidebar collapsed to zero width

API Reference

PropTypeDefaultDescription
open
booleanControlled open state — read from SidebarProvider
defaultOpen
booleantrueInitial open state (uncontrolled)
onOpenChange
functionCallback when open state flips: (open: boolean) => void
side
stringleftWhich 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.

Token budget: 900