bx

Sidebar

A composable, themeable and customizable sidebar component.

Dashboard
Main content zone.
tsx
<div className="w-full max-w-3xl h-[320px] overflow-hidden rounded-lg border">
  <SidebarProvider>
    <Sidebar>
      <SidebarHeader>
        <div className="px-3 py-2 font-semibold">Acme Corp</div>
      </SidebarHeader>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Platform</SidebarGroupLabel>
          <SidebarMenu>
            <SidebarMenuItem><SidebarMenuButton href="#" active>Dashboard</SidebarMenuButton></SidebarMenuItem>
            <SidebarMenuItem><SidebarMenuButton href="#">Projects</SidebarMenuButton></SidebarMenuItem>
            <SidebarMenuItem><SidebarMenuButton href="#">Settings</SidebarMenuButton></SidebarMenuItem>
          </SidebarMenu>
        </SidebarGroup>
      </SidebarContent>
      <SidebarFooter>
        <div className="px-3 py-2 text-xs text-muted-foreground">v1.0.0</div>
      </SidebarFooter>
    </Sidebar>
    <SidebarInset as="div">
      <div className="flex items-center gap-2 border-b px-3 py-2">
        <SidebarTrigger />
        <span className="text-sm font-medium">Dashboard</span>
      </div>
      <div className="p-4 text-sm text-muted-foreground">Main content zone.</div>
    </SidebarInset>
  </SidebarProvider>
</div>

Sidebar is an application sidebar (main navigation) with a header, a footer, groups of menu items and a collapsible state (to an icon rail, or to zero width). It is the reference shadcn pattern for a multi-page dashboard. This port has no separate mobile drawer: the same collapse applies at every width.

Structure: SidebarProvider (root) > Sidebar (left/right) + SidebarInset (main content zone). Sidebar contains Header/Content/Footer, with SidebarGroup > SidebarMenu > SidebarMenuItem > SidebarMenuButton.

Installation

Terminal
$ bext ui add sidebar

Usage

tsx
import { SidebarProvider, Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarInset, SidebarTrigger } from "@bext-stack/ui/primitives"

<SidebarProvider>
  <Sidebar>
    <SidebarHeader>Acme Corp</SidebarHeader>
    <SidebarContent>
      <SidebarGroup>
        <SidebarMenu>
          <SidebarMenuItem>
            <SidebarMenuButton active href="/">Home</SidebarMenuButton>
          </SidebarMenuItem>
        </SidebarMenu>
      </SidebarGroup>
    </SidebarContent>
  </Sidebar>
  <SidebarInset>
    <SidebarTrigger />
    <div>Content</div>
  </SidebarInset>
</SidebarProvider>

Anatomy

<SidebarProvider>
  ├── <Sidebar>                                        // <aside>
  │   ├── <SidebarHeader>Brand</SidebarHeader>
  │   ├── <SidebarContent>                             // <nav aria-label="Sidebar">
  │   │   └── <SidebarGroup>
  │   │       ├── <SidebarGroupLabel>Platform</SidebarGroupLabel>
  │   │       └── <SidebarMenu>
  │   │           └── <SidebarMenuItem>
  │   │               └── <SidebarMenuButton href="/">Home</SidebarMenuButton>
  │   └── <SidebarFooter>Profile</SidebarFooter>
  └── <SidebarInset>                                   // <main>
      ├── <SidebarTrigger />
      └── {main content}

API Reference

SidebarProvider

Prop Type Default Description
defaultOpen boolean true Initial state. false renders the sidebar collapsed.
children ReactNode — A Sidebar and a SidebarInset.
className string — Extra classes on the root (flex min-h-svh w-full).
Prop Type Default Description
side "left" | "right" "left" Sets data-side and which edge gets the border. Place the Sidebar before or after SidebarInset accordingly.
collapsible "offcanvas" | "icon" | "none" "icon" Collapsed width. icon: shrinks to a 3rem rail. offcanvas and none: shrinks to zero width.
id string — id of the <aside>. The triggers point at it with aria-controls. When omitted, the island generates one.
children ReactNode — SidebarHeader, SidebarContent and SidebarFooter.

SidebarHeader / SidebarFooter

Prop Type Default Description
children ReactNode — Header and footer keep their height.

SidebarContent

Prop Type Default Description
ariaLabel string "Sidebar" Accessible name of the <nav> landmark that SidebarContent renders.
children ReactNode — The SidebarGroups. Takes the remaining space and scrolls.

SidebarGroup

Prop Type Default Description
children ReactNode — A section of the content: an optional SidebarGroupLabel and a SidebarMenu.

SidebarGroupLabel

Prop Type Default Description
children ReactNode — Small muted label (text-xs font-medium) above the group.

SidebarMenu

Prop Type Default Description
children ReactNode — The SidebarMenuItems. Rendered as a <ul>.

SidebarMenuItem

Prop Type Default Description
children ReactNode — One SidebarMenuButton. Rendered as an <li>.

SidebarMenuButton

Prop Type Default Description
href string — When set, renders an <a>. Otherwise a <button type="button">.
active boolean false Sets data-active="true" (accent background, medium font weight) and aria-current="page".
children ReactNode — Icon and label.

SidebarTrigger / SidebarRail

Prop Type Default Description
ariaLabel string "Toggle sidebar" Accessible name of the button.
className string — Extra classes on the button that toggles expanded/collapsed (a 28px icon button for SidebarTrigger).

SidebarInset

Prop Type Default Description
as "main" | "div" "main" Element to render. Use "div" when the page already has a <main>.
children ReactNode — Content area next to the sidebar.

Data Attributes

Attributes you can target in custom CSS to override the style for each state.

Attribute On Values Description
data-ui="sidebar" SidebarProvider — Root that the island toggles.
data-state SidebarProvider "expanded" | "collapsed" Current state. The Sidebar width follows it.
data-ui="sidebar-toggle" SidebarTrigger, SidebarRail — Clicking toggles data-state on the closest SidebarProvider.
data-side Sidebar "left" | "right" Reflects the side prop.
data-collapsible Sidebar "offcanvas" | "icon" | "none" Reflects the collapsible prop.
aria-expanded / aria-controls SidebarTrigger, SidebarRail "true" | "false" / id of the <aside> Set and kept in sync by the island.
data-active SidebarMenuButton "true" (absent otherwise) Present on the active item.
data-slot every sub-component "sidebar-provider" | "sidebar" | "sidebar-header" | "sidebar-menu-button" | … Stable selector for custom styling.

Keyboard

Key Action
Ctrl + B / Cmd + B Toggles expanded/collapsed. Handled by the island, on the first sidebar of the page. aria-expanded on the triggers follows.
Enter / Space On SidebarTrigger: toggles expanded/collapsed.

Accessibility

Sidebar renders an <aside> and SidebarInset a <main> (or a <div> with as="div"). SidebarContent renders a <nav> landmark named "Sidebar"; pass ariaLabel to rename it, which is needed when the page has several navigation landmarks.

Each SidebarMenuButton is an <a> or a <button>. The active item carries aria-current="page" in addition to data-active="true".

SidebarTrigger and SidebarRail are buttons named "Toggle sidebar" (ariaLabel to change it). The island sets aria-expanded on them and aria-controls pointing at the <aside>, and keeps aria-expanded in sync on click and on Ctrl/Cmd + B. Both attributes are absent until the island has run.

Try it live Open in play

Edit this example live in the bext playground. It compiles and runs entirely in your browser.

src/app/page.tsx 1 file · react · runs in your browser
The React idiom: same UI, for comparison.