Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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 sidebar
import {
  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

PropTypeDefaultDescription
collapsedbooleanControlled collapsed state. When set, localStorage persistence is skipped.
defaultCollapsedbooleanfalseInitial collapsed state when uncontrolled.
onCollapsedChange(collapsed: boolean) => voidFires on every collapse change (controlled or not).
storageKeystring"ploxum.sidebar"localStorage key for uncontrolled persistence.
childrenReactNodeThe rail plus any consumers of useSidebar().

Sidebar

PropTypeDefaultDescription
collapsedbooleancontext / falseOptional override; otherwise reads useSidebar(). Lets the rail render without a provider.
labelstring"Primary"aria-label for the <nav> landmark.
classNamestringMerged onto the <nav>.
refRef<HTMLElement>Forwarded to the <nav>.

Remaining HTMLAttributes<HTMLElement> pass through.

SidebarMenuButton

PropTypeDefaultDescription
iconReactNodeLeading glyph. Always visible — it is the only content when collapsed.
isActivebooleanfalseActive row: amber-tint background, amber text, and a left accent bar.
tooltipReactNodestring childrenLabel shown in a right-side Tooltip only while collapsed. Falls back to the text children when those are a plain string.
asChildbooleanfalseRender the child element (e.g. an <a> / router Link) as the row, merging styling + the icon/label scaffold.
classNamestringMerged onto the row.
refRef<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 width between 240px (WIDTH_EXPANDED) and 56px (WIDTH_ICON) via the --sidebar-width CSS var with transition-[width] duration-200, gated by motion-reduce. Don't animate row contents.
  • Collapsed labels move to tooltips. When collapsed, SidebarMenuButton hides its text and surfaces the label in a right-anchored Tooltip; SidebarGroupLabel unmounts. 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 sets data-active="true" for styling hooks.
  • State is persisted, but the host owns control. Uncontrolled, the provider seeds from and writes back to localStorage under storageKey. Pass collapsed to 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 compact density (useDensity).
  • SidebarNav (package pattern) vs Sidebar (registry composite). Reach for the package SidebarNav pattern when you want a fixed, always-labelled rail; reach for this registry Sidebar when you need the collapse-to-icon behaviour and want to own the source.

Storybook

Composites/Sidebar ↗