# Sidebar

The navigation column of an admin screen: groups of links that collapse to icons or hide entirely.

- **Import:** `import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger } from "@booleanpress/ui/sidebar"`
- **Page:** <https://ui.booleanpress.com/components/sidebar> · @booleanpress/ui 0.1.0

## Usage

A sidebar is a family of parts. `SidebarProvider` holds the state and wraps both the `Sidebar` and the page beside it, `SidebarInset`. Put a `SidebarTrigger` where people expect the toggle, usually in the page header.

```tsx
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "@booleanpress/ui/sidebar"

export function AdminLayout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <Sidebar collapsible="icon">
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Delivery</SidebarGroupLabel>
            <SidebarMenu>
              <SidebarMenuItem>
                <SidebarMenuButton asChild isActive tooltip="Email log">
                  <a href="/logs">Email log</a>
                </SidebarMenuButton>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>
      <SidebarInset>
        <header>
          <SidebarTrigger />
        </header>
        {children}
      </SidebarInset>
    </SidebarProvider>
  )
}
```

`collapsible` sets what the toggle does: `"icon"` shrinks it to a rail of icons (give each `SidebarMenuButton` a `tooltip` and keep the label in a `span`), `"offcanvas"` (the default) slides it out of view, `"none"` makes it fixed. `side` is `left` or `right`; `variant` is `sidebar`, `floating` or `inset`.

The open or collapsed state is kept in the browser's local storage under the provider's `sidebarStorageKey` (one key per product), and read back on the next visit. It starts open unless `defaultOpen={false}`; a saved choice wins over the default. On the first render the state is `defaultOpen`, and the saved value applies right after. Control it with `open` and `onOpenChange`. `useSidebar()` returns `state`, `open`, `setOpen`, `isMobile`, `openMobile`, `setOpenMobile` and `toggleSidebar`. Ctrl+B or ⌘B toggles it. Below 768 px wide the sidebar becomes a [sheet](/components/sheet) opened by the same trigger.

The sidebar positions itself against the window (`position: fixed`) and the page is at least a window tall, so it belongs in the app's outer layout, once.

## Examples

### Admin layout

Icon-collapsible sidebar with groups, an active item, a badge, a collapsible submenu, and the trigger in the page header.

```tsx
import { ChevronRightIcon, InboxIcon, MailIcon, PlugIcon, RouteIcon, SettingsIcon } from "lucide-react"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@booleanpress/ui/collapsible"
import { Separator } from "@booleanpress/ui/separator"
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
} from "@booleanpress/ui/sidebar"

export default function SidebarAdminLayout() {
  return (
    <SidebarProvider>
      <Sidebar collapsible="icon">
        <nav aria-label="Admin pages" className="flex min-h-0 flex-1 flex-col">
          <SidebarHeader className="px-4 py-3 text-sm font-semibold">BooleanSMTP</SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Delivery</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive tooltip="Email log">
                      <MailIcon />
                      <span>Email log</span>
                    </SidebarMenuButton>
                    <SidebarMenuBadge>12</SidebarMenuBadge>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Mailers">
                      <PlugIcon />
                      <span>Mailers</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Routing rules">
                      <RouteIcon />
                      <span>Routing rules</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
            <SidebarGroup>
              <SidebarGroupLabel>Configure</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <Collapsible asChild defaultOpen className="group/collapsible">
                    <SidebarMenuItem>
                      <CollapsibleTrigger asChild>
                        <SidebarMenuButton tooltip="Settings">
                          <SettingsIcon />
                          <span>Settings</span>
                          <ChevronRightIcon className="ms-auto transition-transform group-data-[state=open]/collapsible:rotate-90 rtl:rotate-180 rtl:group-data-[state=open]/collapsible:rotate-90" />
                        </SidebarMenuButton>
                      </CollapsibleTrigger>
                      <CollapsibleContent>
                        <SidebarMenuSub>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton href="#">
                              <span>Logging</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton href="#">
                              <span>Notifications</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                        </SidebarMenuSub>
                      </CollapsibleContent>
                    </SidebarMenuItem>
                  </Collapsible>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarRail />
        </nav>
      </Sidebar>
      <SidebarInset>
        <header className="flex h-12 items-center gap-2 border-b px-3">
          <SidebarTrigger />
          <Separator orientation="vertical" className="h-4" />
          <span className="text-sm font-medium">Email log</span>
        </header>
        <div className="flex items-center gap-2 p-4 text-sm text-muted-foreground">
          <InboxIcon className="size-4" aria-hidden="true" />
          Press Ctrl+B or ⌘B to collapse the sidebar to its icons.
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

### Starting collapsed

`defaultOpen={false}` starts it as a rail of icons until someone opens it.

```tsx
import { PlugIcon, UsersIcon } from "lucide-react"
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "@booleanpress/ui/sidebar"

export default function SidebarDefaultClosed() {
  return (
    <SidebarProvider defaultOpen={false}>
      <Sidebar collapsible="icon">
        <nav aria-label="Admin pages" className="flex min-h-0 flex-1 flex-col">
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive tooltip="People">
                      <UsersIcon />
                      <span>People</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Organizations">
                      <PlugIcon />
                      <span>Organizations</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
        </nav>
      </Sidebar>
      <SidebarInset>
        <header className="flex h-12 items-center gap-2 border-b px-3">
          <SidebarTrigger />
          <span className="text-sm font-medium">People</span>
        </header>
        <p className="p-4 text-sm text-muted-foreground">It starts as a rail of icons. The choice is kept in this browser afterwards.</p>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

### On the right

`side="right"` with `collapsible="offcanvas"`: a detail panel that slides out of view.

```tsx
import { TicketIcon } from "lucide-react"
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "@booleanpress/ui/sidebar"

export default function SidebarRightSide() {
  return (
    <SidebarProvider>
      <Sidebar side="right" variant="sidebar" collapsible="offcanvas">
        <nav aria-label="Ticket details" className="flex min-h-0 flex-1 flex-col">
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Details</SidebarGroupLabel>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuButton>
                    <TicketIcon />
                    <span>Related tickets</span>
                  </SidebarMenuButton>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
        </nav>
      </Sidebar>
      <SidebarInset>
        <header className="flex h-12 items-center justify-between border-b px-3">
          <span className="text-sm font-medium">Ticket 1042</span>
          <SidebarTrigger />
        </header>
        <p className="p-4 text-sm text-muted-foreground">The panel sits on the right and slides out of view.</p>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

## Accessibility

**Semantics.** A plain container: put the links in a `nav` yourself by passing `asChild` to `SidebarMenuButton` with a link, or wrap the sidebar's content in a `nav` with a name. `SidebarMenuButton` renders a `button`; `isActive` sets `data-active`, not `aria-current`. Menus are `ul` lists. On narrow windows the sidebar is a modal dialog (a sheet) named from the provider's `sidebarTitle` and described by `sidebarDescription`.

**Labels.** The trigger and the rail are named from the provider's `toggleSidebar` string. An icon-only button in the collapsed rail is named by its text, which stays in the DOM (hidden visually), and by its tooltip. Pass `aria-current="page"` yourself on the link of the current page.

**Focus.** The trigger and every menu button are in the tab order. The rail is a mouse affordance and is not focusable (`tabIndex={-1}`). On narrow windows the sheet moves focus in, keeps Tab inside and returns focus to the trigger.

**Known limits.**

- `isActive` only styles the item; it does not set `aria-current`. Set it on the link yourself.
- It positions itself against the window, so it cannot sit inside a smaller panel. The examples render each in its own frame.
- A tooltip shows only in the collapsed rail, never when the sidebar is open or on narrow windows.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Ctrl + B | Toggles the sidebar from anywhere on the page. ⌘B on macOS. |
| Enter + Space | On the trigger or a menu button, presses it. |
| Tab | Moves through the trigger and the menu buttons in order. |
| Escape | On narrow windows, closes the sheet and returns focus to the trigger. |

## API

### Sidebar

Renders a `div` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `collapsible` | `"none" \| "icon" \| "offcanvas"` | `offcanvas` | What the toggle does: `offcanvas` slides it out of view, `icon` leaves a rail of icons, `none` keeps it fixed. |
| `side` | `"left" \| "right"` | `left` | `left` or `right`. `left` by default. |
| `variant` | `"inset" \| "sidebar" \| "floating"` | `sidebar` | `sidebar` (full height, a border), `floating` (a card with a margin) or `inset` (the page sits in a card beside it). |

### SidebarContent

Renders a `div` and passes it every other prop.

### SidebarFooter

Renders a `div` and passes it every other prop.

### SidebarGroup

Renders a `div` and passes it every other prop.

### SidebarGroupAction

Renders a `button` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the child element instead, with this part's classes merged onto it. |

### SidebarGroupContent

Renders a `div` and passes it every other prop.

### SidebarGroupLabel

Renders a `div` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the child element instead, with this part's classes merged onto it. |

### SidebarHeader

Renders a `div` and passes it every other prop.

### SidebarInput

### SidebarInset

Renders a `main` and passes it every other prop.

### SidebarMenu

Renders a `ul` and passes it every other prop.

### SidebarMenuAction

Renders a `button` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the child element instead, with this part's classes merged onto it. |
| `showOnHover` | `boolean` | `false` | Show it only while the row is hovered or has focus. |

### SidebarMenuBadge

Renders a `div` and passes it every other prop.

### SidebarMenuButton

Renders a `button` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the child element instead, such as a link, with this part's classes merged onto it. |
| `isActive` | `boolean` | `false` | Marks the current page (`data-active`) and highlights it. |
| `tooltip` | `string \| (TooltipContentProps & RefAttributes<HTMLDivElement>)` |  | Text, or `TooltipContent` props, shown beside the button while the sidebar is collapsed to icons. |

### SidebarMenuItem

Renders a `li` and passes it every other prop.

### SidebarMenuSkeleton

Renders a `div` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `showIcon` | `boolean` | `false` | Draw a square where the icon goes. |

### SidebarMenuSub

Renders a `ul` and passes it every other prop.

### SidebarMenuSubButton

Renders a `a` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the child element instead, such as a router link. |
| `isActive` | `boolean` | `false` | Marks the current page and highlights it. |
| `size` | `"sm" \| "md"` | `md` | `sm` or `md`. `md` by default. |

### SidebarMenuSubItem

Renders a `li` and passes it every other prop.

### SidebarProvider

Renders a `div` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` | `true` | Whether it starts open. `true` by default; a choice saved in local storage wins. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with `true` or `false` when it opens or collapses. |
| `open` | `boolean` |  | Whether it is open, when you control it. Pair it with `onOpenChange`. |

### SidebarRail

Renders a `button` and passes it every other prop.

### SidebarSeparator

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `decorative` | `boolean` |  | Whether or not the component is purely decorative. When true, accessibility-related attributes are updated so that that the rendered element is removed from the accessibility tree. |
| `orientation` | `"horizontal" \| "vertical"` | `vertical` | Either `vertical` or `horizontal`. Defaults to `horizontal`. |

### SidebarTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

**Also exported:** `useSidebar`, a hook for the parts' shared state; call it inside the component's provider.

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="sidebar"` (Sidebar), `data-slot="sidebar-content"` (SidebarContent), `data-slot="sidebar-footer"` (SidebarFooter), `data-slot="sidebar-group"` (SidebarGroup), `data-slot="sidebar-group-action"` (SidebarGroupAction), `data-slot="sidebar-group-content"` (SidebarGroupContent), `data-slot="sidebar-group-label"` (SidebarGroupLabel), `data-slot="sidebar-header"` (SidebarHeader), `data-slot="sidebar-input"` (SidebarInput), `data-slot="sidebar-inset"` (SidebarInset), `data-slot="sidebar-menu"` (SidebarMenu), `data-slot="sidebar-menu-action"` (SidebarMenuAction), `data-slot="sidebar-menu-badge"` (SidebarMenuBadge), `data-slot="sidebar-menu-button"` (SidebarMenuButton), `data-slot="sidebar-menu-item"` (SidebarMenuItem), `data-slot="sidebar-menu-skeleton"` (SidebarMenuSkeleton), `data-slot="sidebar-menu-sub"` (SidebarMenuSub), `data-slot="sidebar-menu-sub-button"` (SidebarMenuSubButton), `data-slot="sidebar-menu-sub-item"` (SidebarMenuSubItem), `data-slot="sidebar-wrapper"` (SidebarProvider), `data-slot="sidebar-rail"` (SidebarRail), `data-slot="sidebar-separator"` (SidebarSeparator), `data-slot="sidebar-trigger"` (SidebarTrigger), and `data-state`, `data-collapsible`, `data-variant`, `data-side`, `data-size`, `data-active`.

**Provider strings:** `sidebarTitle`, `sidebarDescription`, `toggleSidebar` (`BooleanUIProvider`'s `strings`).

## Theming

The sidebar has its own tokens: `--sidebar`, `--sidebar-foreground`, `--sidebar-accent`, `--sidebar-accent-foreground`, `--sidebar-border` and `--sidebar-ring`. Width is `--sidebar-width` (16rem) and `--sidebar-width-icon` (3rem) on the provider; override them with `style`. On narrow windows the sheet is 18rem wide.

| Token | Used for |
| --- | --- |
| `--background` | background |
| `--sidebar` | background |
| `--sidebar-accent` | background |
| `--sidebar-accent-foreground` | text |
| `--sidebar-border` | border, background |
| `--sidebar-foreground` | text |
| `--sidebar-ring` | focus ring |
