---
id: "packages/active-ui/sidebar"
title: "Sidebar"
description: "The navigation column of an application, collapsible on a wide screen and a sheet on a small one, with groups, menus, badges and submenus."
url: "/packages/active-ui/sidebar"
locale: "en"
revision: "7d9e0b66f97b0314"
---

# Sidebar

The sidebar is the navigation column of an application: a header, groups of menu items, and a
footer, beside the page. On a wide screen it can collapse out of the way or to icons; below the
`md` width it becomes a [sheet](/packages/active-ui/sheet) that the reader opens with a trigger.
The navigation of this site is a sidebar. The example shows its parts in a fixed frame.

**Example: The parts of a sidebar**

Client: Runs entirely in your browser. Source: `examples/ui/sidebar.tsx`.

```tsx
// apps/docs/examples/ui/sidebar.tsx
'use client';

import {useState} from 'react';
import {
  FileTextIcon,
  HomeIcon,
  PlusIcon,
  SettingsIcon,
  UsersIcon,
} from 'lucide-react';

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
} from '@workspace/active-ui/components/primitives/sidebar';

const MAIN = [
  {label: 'Home', icon: HomeIcon},
  {label: 'Participants', icon: UsersIcon, badge: '12'},
  {label: 'Schemes', icon: FileTextIcon},
];

/**
 * The parts of a sidebar, in a frame of its own: a header, a labelled group
 * with an action, menu buttons with icons and a badge, a submenu, and a
 * footer. `collapsible="none"` keeps it in place inside this example; the
 * navigation of this site is the same component in its full form, with the
 * sheet on small screens.
 */
export default function SidebarExample() {
  const [current, setCurrent] = useState('Participants');
  const pick = (label: string) => () => setCurrent(label);
  return (
    <SidebarProvider className="min-h-0 w-auto">
      <Sidebar
        collapsible="none"
        mobileLabel="Example navigation"
        className="h-96 rounded-lg border"
      >
        <SidebarHeader>
          <p className="px-2 py-1 font-semibold">Pension office</p>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Work</SidebarGroupLabel>
            <SidebarGroupAction aria-label="Add a scheme" title="Add a scheme">
              <PlusIcon aria-hidden />
            </SidebarGroupAction>
            <SidebarGroupContent>
              <SidebarMenu>
                {MAIN.map(({label, icon: Icon, badge}) => (
                  <SidebarMenuItem key={label}>
                    <SidebarMenuButton
                      isActive={current === label}
                      aria-current={current === label ? 'page' : undefined}
                      onClick={pick(label)}
                    >
                      <Icon aria-hidden />
                      <span>{label}</span>
                    </SidebarMenuButton>
                    {badge ? (
                      <SidebarMenuBadge aria-label={`${badge} new`}>
                        {badge}
                      </SidebarMenuBadge>
                    ) : null}
                  </SidebarMenuItem>
                ))}
                <SidebarMenuItem>
                  <SidebarMenuButton
                    isActive={current.startsWith('Settings')}
                    onClick={pick('Settings · General')}
                  >
                    <SettingsIcon aria-hidden />
                    <span>Settings</span>
                  </SidebarMenuButton>
                  <SidebarMenuSub>
                    {['General', 'Users'].map((sub) => (
                      <SidebarMenuSubItem key={sub}>
                        <SidebarMenuSubButton
                          render={<button type="button" />}
                          isActive={current === `Settings · ${sub}`}
                          onClick={pick(`Settings · ${sub}`)}
                        >
                          {sub}
                        </SidebarMenuSubButton>
                      </SidebarMenuSubItem>
                    ))}
                  </SidebarMenuSub>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>
          <p className="text-muted-foreground px-2 text-xs">
            Showing: {current}
          </p>
        </SidebarFooter>
      </Sidebar>
    </SidebarProvider>
  );
}
```

## Usage

```tsx
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from '@workspace/active-ui/components/primitives/sidebar';

<SidebarProvider>
  <Sidebar mobileLabel="Navigation">
    <SidebarContent>
      <SidebarGroup>
        <SidebarGroupLabel>Work</SidebarGroupLabel>
        <SidebarGroupContent>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton isActive render={<Link href="/schemes" />}>
                Schemes
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarGroupContent>
      </SidebarGroup>
    </SidebarContent>
  </Sidebar>
  <SidebarInset className="min-w-0">
    <header>
      <SidebarTrigger label="Toggle navigation" />
    </header>
    {page}
  </SidebarInset>
</SidebarProvider>;
```

## Layout

- `SidebarProvider` wraps the whole layout: it holds the open state, knows whether the screen is
  narrow ([`useIsMobile`](/packages/active-ui/hooks), below `768px`), and draws a flex row at least as high as the viewport.
- `Sidebar` is the column, `16rem` wide (`18rem` as a sheet):

  - `side`: `left` (default) or `right`;
  - `variant`: `sidebar` (default, flush with the edge), `floating` (a card with a margin) or
    `inset` (the page becomes the card);
  - `collapsible`: `offcanvas` (default, slides out completely), `icon` (shrinks to `3rem`, icons
    only, labels as tooltips) or `none` (always shown, as in the example).
- `SidebarInset` is the page beside it, a `<main>`. **Give it `min-w-0`**: it is a flex item, and
  without it the widest line of a page (a long code line, a wide table) sets its minimum width and
  pushes the page past the viewport on a narrow desktop.
- `SidebarTrigger` toggles the sidebar, or opens the sheet on a small screen. `SidebarRail` is a
  thin handle on the sidebar's edge that toggles it too.

## The menu

- `SidebarHeader`, `SidebarContent` (the scrolling part) and `SidebarFooter`.
- `SidebarGroup` with `SidebarGroupLabel`, `SidebarGroupAction` and `SidebarGroupContent`.
- `SidebarMenu` (`<ul>`) with `SidebarMenuItem` (`<li>`):

  - `SidebarMenuButton`: the item. `isActive` marks the current page with a calm fill that stays
    while the pointer rests on another item; hover has half that fill, and keyboard focus its own
    ring. `render` makes your router's link the element; `tooltip` shows a label when the sidebar
    is collapsed to icons; `size` and `variant` change its height and look.
  - `SidebarMenuAction`: a button at the end of an item (`showOnHover` to reveal it on hover only).
  - `SidebarMenuBadge`: a count at the end of an item.
  - `SidebarMenuSub` with `SidebarMenuSubItem` and `SidebarMenuSubButton`: an indented submenu.
- `SidebarInput` and `SidebarSeparator`: a search field and a line, styled for the sidebar.

## State and the shortcut

- Uncontrolled by default (`defaultOpen`, default `true`); controlled with `open` and
  `onOpenChange`. The sheet on a small screen has its own state.
- `useSidebar()` returns the state and its setters (`state`, `open`, `setOpen`, `openMobile`,
  `setOpenMobile`, `isMobile`, `toggleSidebar`) to parts of your own inside the provider.
- `Ctrl+B` (or `⌘B`) toggles the sidebar. The provider listens on the window, so use one provider
  per page.
- The provider stores nothing. To keep the open state across page loads, pass
  `persistOpenState`: it is called with every new state, and you decide where it goes, such as a
  cookie your server reads back into `defaultOpen`.

## What your application provides

- `mobileLabel`: the accessible name of the sheet on a small screen (required), and `label` of
  `SidebarTrigger` and `SidebarRail`.
- The items, their links and the current page (`isActive`, plus `aria-current="page"` on the
  link).
- Closing the sheet after a choice on a small screen: call `setOpenMobile(false)` when an item is
  chosen, as this site does.

## Accessibility and keyboard

- The menu items are links or buttons in lists; `Tab` moves through them.
- On a small screen the sidebar is a modal sheet named by `mobileLabel`: the focus moves into it,
  and `Escape` closes it.
- The current item is recognisable without colour alone through `aria-current` for screen readers;
  the fill is the visual cue.

## API

### SidebarProvider

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` | no | Whether the desktop sidebar starts open, when uncontrolled. |
| `onOpenChange` | `((open: boolean) => void)` | no | Called with the new desktop open state. |
| `open` | `boolean` | no | Whether the desktop sidebar is open, when controlled. |
| `persistOpenState` | `((open: boolean) => void)` | no | Keeps the desktop open state across page loads: called with every new state, controlled or not. The host owns where and how (a cookie its server reads back into `defaultOpen`, storage, nothing at all). Without it the provider stores nothing; it never writes a cookie of its own. |

### Sidebar

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `mobileLabel` | `string` | yes | The accessible name of the sheet the sidebar becomes on a small screen. |
| `collapsible` | `"icon" \| "none" \| "offcanvas"` | no | How it collapses on a wide screen: `offcanvas` slides out completely, `icon` shrinks to its icons, `none` stays in place. |
| `side` | `"left" \| "right"` | no | The edge of the screen the sidebar sits on. |
| `variant` | `"floating" \| "inset" \| "sidebar"` | no | `sidebar` is flush with the edge; `floating` a card with a margin; `inset` makes the page beside it the card. |

### SidebarMenuButton

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `isActive` | `boolean` | no | Whether the item is the current page: it keeps the full accent fill. |
| `size` | `"default" \| "lg" \| "sm" \| null` | no | The height: `sm`, `default` or `lg` (for an item with two lines). |
| `tooltip` | `string \| (TooltipPopupProps & Pick<TooltipPositionerProps, "align" \| "alignOffset" \| "side" \| "sideOffset">)` | no | The label shown as a tooltip while the sidebar is collapsed to icons; a string, or the props of `TooltipContent`. |
| `variant` | `"default" \| "outline" \| null` | no | `default`, or `outline` with a border and the page background. |
| `render` | `ComponentRenderFn<HTMLProps, {}> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### Other parts

**SidebarContent**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarContent has no props of its own.

**SidebarFooter**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarFooter has no props of its own.

**SidebarGroup**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarGroup has no props of its own.

**SidebarGroupAction**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, {}> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**SidebarGroupContent**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarGroupContent has no props of its own.

**SidebarGroupLabel**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, {}> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**SidebarHeader**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarHeader has no props of its own.

**SidebarInput**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarInput has no props of its own.

**SidebarInset**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarInset has no props of its own.

**SidebarMenu**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarMenu has no props of its own.

**SidebarMenuAction**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `showOnHover` | `boolean` | no | Shows the action only while its item is hovered or focused, on a wide screen. |
| `render` | `ComponentRenderFn<HTMLProps, {}> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**SidebarMenuBadge**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarMenuBadge has no props of its own.

**SidebarMenuItem**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarMenuItem has no props of its own.

**SidebarMenuSub**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarMenuSub has no props of its own.

**SidebarMenuSubButton**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `isActive` | `boolean` | no | Whether the subitem is the current page. |
| `size` | `"md" \| "sm"` | no | The text size of the subitem. |
| `render` | `ComponentRenderFn<HTMLProps, {}> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**SidebarMenuSubItem**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

SidebarMenuSubItem has no props of its own.

**SidebarRail**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | yes | The accessible name of the handle, such as "Toggle navigation". |

**SidebarSeparator**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `orientation` | `Orientation` | no | The orientation of the separator. |
| `render` | `ComponentRenderFn<HTMLProps, SeparatorState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**SidebarTrigger**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | yes | The accessible name of the button, such as "Toggle navigation". |
| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs" \| null` | no | The height and padding. `xs`, `sm`, `default` and `lg` for a button with text; `icon-xs`, `icon-sm`, `icon` and `icon-lg` for a square button with only an icon, which then needs an accessible name (`aria-label`). |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | The visual weight of the button. `default` for the main action of a screen or form; `outline` and `secondary` for other actions; `ghost` for a quiet action in a toolbar or a list; `destructive` for an action that deletes or cannot be undone; `link` for an action that reads as a link. Every variant but `link` sinks 1px while pressed; a link-styled action keeps its text and icon in place (`button-link-still`). |
| `focusableWhenDisabled` | `boolean` | no | Whether the button should be focusable when disabled. |
| `nativeButton` | `boolean` | no | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ComponentRenderFn<HTMLProps, ButtonState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**useSidebar**: `() => SidebarContextProps`

The state of the nearest `SidebarProvider` and its setters: `state`
(`expanded` or `collapsed`), `open` and `setOpen` for the desktop sidebar,
`openMobile` and `setOpenMobile` for the sheet on a small screen,
`isMobile`, and `toggleSidebar`, which toggles whichever is in use. Throws
outside a provider.

Returns an object with:

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `state` | `"collapsed" \| "expanded"` | yes |  |
| `open` | `boolean` | yes |  |
| `setOpen` | `(open: boolean) => void` | yes |  |
| `openMobile` | `boolean` | yes |  |
| `setOpenMobile` | `(open: boolean) => void` | yes |  |
| `isMobile` | `boolean` | yes |  |
| `toggleSidebar` | `() => void` | yes |  |

> [!NOTE]
> **Based on shadcn/ui**
>
> This page adapts the Sidebar documentation of shadcn/ui (MIT licence) to this
> package; see [Third-party notices](/third-party-notices).
