Sidebar
A composable, themeable and customizable sidebar component.
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
$ bext ui add sidebar
Copy and paste the following into src/components/ui/sidebar.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="sidebar" hooks.
Usage
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). |
Sidebar
| 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.
Edit this example live in the bext playground. It compiles and runs entirely in your browser.
On This Page