# Sidebar

An accessible React sidebar for Next.js and Tailwind CSS: collapses to icons or off canvas, with a gliding active indicator and a sheet on phones.

Source: https://ui.fewtone.com/components/sidebar

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/sidebar.json
```

## Usage

```tsx
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "@/components/ui/sidebar"
import { HomeIcon, InboxIcon } from "lucide-react"
import Link from "next/link"

export function AppShell({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <Sidebar collapsible="icon">
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Application</SidebarGroupLabel>
            <SidebarMenu>
              <SidebarMenuItem>
                <SidebarMenuButton asChild isActive tooltip="Home">
                  <Link href="/">
                    <HomeIcon />
                    <span>Home</span>
                  </Link>
                </SidebarMenuButton>
              </SidebarMenuItem>
              <SidebarMenuItem>
                <SidebarMenuButton asChild tooltip="Inbox">
                  <Link href="/inbox">
                    <InboxIcon />
                    <span>Inbox</span>
                  </Link>
                </SidebarMenuButton>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>
      <SidebarInset>
        <header className="flex h-14 items-center px-4">
          <SidebarTrigger />
        </header>
        {children}
      </SidebarInset>
    </SidebarProvider>
  )
}
```

## API reference

### SidebarProvider

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | – | Whether the sidebar is open, when you control it. |
| `defaultOpen` | `boolean` | `true` | Whether it starts open. |
| `onOpenChange` | `(open: boolean) => void` | – | Called when it opens or collapses, for example to save the choice. The sheet on phones keeps its own state. |
| `shortcut` | `string \| false` | `"b"` | The letter that toggles the sidebar with ⌘ or Ctrl. It's ignored while typing in a field. false turns it off, for example when a page shows several sidebars. |

### Sidebar

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `collapsible` | `"offcanvas" \| "icon" \| "none"` | `"offcanvas"` | What collapsing does: slide it away, narrow it to its icons (labels become tooltips), or nothing. |
| `side` | `"left" \| "right"` | `"left"` | The edge it's on. Put a right sidebar after SidebarInset. |
| `aria-label` | `string` | `"Sidebar"` | The landmark's name, and the sheet's title on phones. |

### SidebarTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarRail

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarInset

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the single child instead of a main, when the page already has one. |

### SidebarHeader

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarFooter

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarGroupLabel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the single child (for example a button) as the label. |

### SidebarGroupContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarMenu

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarMenuItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarMenuButton

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `isActive` | `boolean` | `false` | Marks the current page: the highlight glides behind it, and it gets aria-current="page". |
| `tooltip` | `string \| TooltipContent props` | – | Names the row in a tooltip while the sidebar is down to its icons. It doesn't show otherwise. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Row height: 28, 32 or 48px. lg suits a team switcher or a user, with a 32px picture first. |
| `asChild` | `boolean` | `false` | Render the single child (for example a Next.js Link) as the row. On phones, following a link closes the sheet. |

### SidebarMenuBadge

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarMenuCollapsible

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | – | Whether the section is open, when you control it. |
| `defaultOpen` | `boolean` | `false` | Whether it starts open. |
| `onOpenChange` | `(open: boolean) => void` | – | Called when it opens or closes. |
| `disabled` | `boolean` | `false` | Stops it opening or closing. |

### SidebarMenuCollapsibleTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarMenuCollapsibleContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarMenuSub

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarMenuSubItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### SidebarMenuSubButton

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `isActive` | `boolean` | `false` | Marks the current page, with aria-current="page". |
| `size` | `"sm" \| "md"` | `"md"` | Text size: text-xs or text-sm. |
| `asChild` | `boolean` | `false` | Render the single child (for example a Next.js Link) as the sub-row. |

### SidebarSeparator

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### useSidebar

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

**When to use it:** Use Sidebar for an app's main navigation beside the page: a dashboard or settings area with groups of links, collapsing to icons or off canvas, and a sheet on phones. Mark the current page with isActive. For a short set of views inside a page use Tabs; for a panel of actions or a form use Sheet.
**Built on:** Radix Collapsible, Radix Dialog (Sheet), Radix Tooltip.
**Accessibility pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
**npm packages (installed by the CLI):** class-variance-authority@^0.7.1, lucide-react@^1.52.0, motion@^14.0.0, radix-ui@^1.7.0.
**Installs these FewTone UI components with it:** `button`, `sheet`, `tooltip`.

## Examples

### An app shell

Pick a page and the highlight glides to it. Collapse with the trigger, the rail on the sidebar's edge or ⌘B / Ctrl+B: down to icons, the labels leave through a soft blur, the Settings section folds and its row holds the highlight, and each icon names itself in a tooltip. On a phone the sidebar opens as a sheet.

```tsx
"use client";

import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuCollapsible,
  SidebarMenuCollapsibleContent,
  SidebarMenuCollapsibleTrigger,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar";
import {
  AudioWaveformIcon,
  BadgeCheckIcon,
  BellIcon,
  ChartPieIcon,
  ChevronsUpDownIcon,
  CommandIcon,
  CreditCardIcon,
  FolderIcon,
  FrameIcon,
  GalleryVerticalEndIcon,
  InboxIcon,
  LayoutDashboardIcon,
  LogOutIcon,
  type LucideIcon,
  MapIcon,
  PlusIcon,
  Settings2Icon,
} from "lucide-react";
import { type MouseEvent, useId, useState } from "react";

type Team = { name: string; plan: string; logo: LucideIcon };

const teams: [Team, ...Team[]] = [
  { name: "Acme Inc", plan: "Enterprise", logo: GalleryVerticalEndIcon },
  { name: "Acme Corp.", plan: "Startup", logo: AudioWaveformIcon },
  { name: "Evil Corp.", plan: "Free", logo: CommandIcon },
];

const user = { name: "Olivia Martin", email: "olivia@acme.com", initials: "OM" };

/**
 * Where a sidebar menu opens: open, along the sidebar (below the team, above the user) at the trigger's width,
 * so it reads as part of the panel; down to icons, beside the rail, since there's no room under a 32px picture
 */
function useMenuPlacement(along: "bottom" | "top") {
  const { state, isMobile } = useSidebar();
  const beside = state === "collapsed" && !isMobile;
  return {
    side: beside ? ("right" as const) : along,
    align: beside && along === "top" ? ("end" as const) : ("start" as const),
    sideOffset: beside ? 8 : 4,
    className: "w-(--radix-dropdown-menu-trigger-width) min-w-60",
  };
}

/** The team switcher: the current team's logo, name and plan; a short menu to switch teams or add one */
function TeamSwitcher({ team, onTeamChange }: { team: Team; onTeamChange: (team: Team) => void }) {
  const placement = useMenuPlacement("bottom");
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <SidebarMenuButton size="lg">
          <span className="flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
            <team.logo className="size-(--size-icon-md)" />
          </span>
          <span className="grid flex-1 text-start leading-tight">
            <span className="truncate font-medium text-foreground">{team.name}</span>
            <span className="truncate text-xs">{team.plan}</span>
          </span>
          <ChevronsUpDownIcon className="ms-auto" />
        </SidebarMenuButton>
      </DropdownMenuTrigger>
      <DropdownMenuContent {...placement}>
        <DropdownMenuLabel>Teams</DropdownMenuLabel>
        <DropdownMenuRadioGroup
          value={team.name}
          onValueChange={(name) => onTeamChange(teams.find((option) => option.name === name) ?? team)}
        >
          {teams.map((option) => (
            <DropdownMenuRadioItem key={option.name} value={option.name}>
              {/* Each team's own logo on a soft tile, and its name: one quiet line (the plan is on the trigger) */}
              <span className="flex size-5 shrink-0 items-center justify-center rounded-sm bg-muted">
                <option.logo className="size-3" />
              </span>
              <span className="truncate">{option.name}</span>
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem className="text-muted-foreground">
          <PlusIcon />
          Add team
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

/** The signed-in user: who it is at the top of the menu, their account pages, and signing out */
function NavUser() {
  const placement = useMenuPlacement("top");
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <SidebarMenuButton size="lg">
          <Avatar>
            <AvatarFallback>{user.initials}</AvatarFallback>
          </Avatar>
          <span className="grid flex-1 text-start leading-tight">
            <span className="truncate font-medium text-foreground">{user.name}</span>
            <span className="truncate text-xs">{user.email}</span>
          </span>
          <ChevronsUpDownIcon className="ms-auto" />
        </SidebarMenuButton>
      </DropdownMenuTrigger>
      <DropdownMenuContent {...placement}>
        <DropdownMenuLabel className="flex items-center gap-2 px-1.5 py-1.5 text-sm text-foreground">
          <Avatar>
            <AvatarFallback>{user.initials}</AvatarFallback>
          </Avatar>
          <span className="grid min-w-0 leading-tight">
            <span className="truncate font-medium">{user.name}</span>
            <span className="truncate text-xs text-muted-foreground">{user.email}</span>
          </span>
        </DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem>
            <BadgeCheckIcon />
            Account
          </DropdownMenuItem>
          <DropdownMenuItem>
            <CreditCardIcon />
            Billing
          </DropdownMenuItem>
          <DropdownMenuItem>
            <BellIcon />
            Notifications
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive">
          <LogOutIcon />
          Log out
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

const platform = [
  { title: "Dashboard", icon: LayoutDashboardIcon },
  { title: "Inbox", icon: InboxIcon, badge: "12" },
  { title: "Projects", icon: FolderIcon },
];

const settings = ["General", "Team", "Billing"];

const projects = [
  { title: "Design Engineering", icon: FrameIcon },
  { title: "Sales & Marketing", icon: ChartPieIcon },
  { title: "Travel", icon: MapIcon },
];

type Collapse = "icon" | "offcanvas";

/*
 * An app shell in a fixed-height frame: a team switcher, two groups (Settings opens a sub-menu), the user in
 * the footer, and the page beside it with the trigger. The team and user menus open along the sidebar at its
 * width, or beside the rail once it's down to icons. Pick a page and the highlight glides to it; collapse
 * with the trigger, the rail on the edge or ⌘B / Ctrl+B. What collapsing does is set at the top right.
 */
export default function SidebarAppShell() {
  const id = useId();
  const [team, setTeam] = useState(teams[0]);
  const [page, setPage] = useState("Dashboard");
  const [collapsible, setCollapsible] = useState<Collapse>("icon");

  // The links switch this preview's page instead of leaving the docs
  const go = (title: string) => (event: MouseEvent) => {
    event.preventDefault();
    setPage(title);
  };

  return (
    <SidebarProvider className="h-128 min-h-0">
      <Sidebar collapsible={collapsible}>
        <SidebarHeader>
          <SidebarMenu>
            <SidebarMenuItem>
              <TeamSwitcher team={team} onTeamChange={setTeam} />
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarHeader>

        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Platform</SidebarGroupLabel>
            <SidebarMenu>
              {platform.map((item) => (
                <SidebarMenuItem key={item.title}>
                  <SidebarMenuButton asChild isActive={page === item.title} tooltip={item.title}>
                    <a href={`#${item.title.toLowerCase()}`} onClick={go(item.title)}>
                      <item.icon />
                      <span>{item.title}</span>
                    </a>
                  </SidebarMenuButton>
                  {item.badge ? <SidebarMenuBadge>{item.badge}</SidebarMenuBadge> : null}
                </SidebarMenuItem>
              ))}
              <SidebarMenuCollapsible defaultOpen>
                <SidebarMenuCollapsibleTrigger tooltip="Settings">
                  <Settings2Icon />
                  <span>Settings</span>
                </SidebarMenuCollapsibleTrigger>
                <SidebarMenuCollapsibleContent>
                  <SidebarMenuSub>
                    {settings.map((title) => (
                      <SidebarMenuSubItem key={title}>
                        <SidebarMenuSubButton
                          href={`#${title.toLowerCase()}`}
                          isActive={page === title}
                          onClick={go(title)}
                        >
                          <span>{title}</span>
                        </SidebarMenuSubButton>
                      </SidebarMenuSubItem>
                    ))}
                  </SidebarMenuSub>
                </SidebarMenuCollapsibleContent>
              </SidebarMenuCollapsible>
            </SidebarMenu>
          </SidebarGroup>

          <SidebarGroup>
            <SidebarGroupLabel>Projects</SidebarGroupLabel>
            <SidebarMenu>
              {projects.map((item) => (
                <SidebarMenuItem key={item.title}>
                  <SidebarMenuButton asChild isActive={page === item.title} tooltip={item.title}>
                    <a href={`#${item.title.toLowerCase()}`} onClick={go(item.title)}>
                      <item.icon />
                      <span>{item.title}</span>
                    </a>
                  </SidebarMenuButton>
                </SidebarMenuItem>
              ))}
            </SidebarMenu>
          </SidebarGroup>
        </SidebarContent>

        <SidebarFooter>
          <SidebarMenu>
            <SidebarMenuItem>
              <NavUser />
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarFooter>
        <SidebarRail />
      </Sidebar>

      {/* In your app the inset is the page's <main>; this preview sits inside the docs page's own */}
      <SidebarInset asChild>
        <div>
          <header className="flex h-14 shrink-0 items-center gap-2 px-4">
            <SidebarTrigger className="-ms-1" />
            <div aria-hidden className="me-2 h-4 w-px shrink-0 self-center bg-border" />
            <p className="truncate text-sm font-medium">{page}</p>
            <div className="ms-auto hidden items-center gap-2 md:flex">
              <label htmlFor={`${id}-collapse`} className="text-sm text-muted-foreground">
                Collapse
              </label>
              <Select value={collapsible} onValueChange={(value) => setCollapsible(value as Collapse)}>
                <SelectTrigger id={`${id}-collapse`} size="sm" className="w-32">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="icon">To icons</SelectItem>
                  <SelectItem value="offcanvas">Off canvas</SelectItem>
                </SelectContent>
              </Select>
            </div>
          </header>
          <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-auto px-4 pb-4">
            <div className="grid grid-cols-3 gap-4">
              <div className="aspect-video rounded-lg bg-muted/50" />
              <div className="aspect-video rounded-lg bg-muted/50" />
              <div className="aspect-video rounded-lg bg-muted/50" />
            </div>
            <div className="flex flex-1 items-start rounded-lg bg-muted/50 p-4">
              <p className="text-sm text-muted-foreground">
                Toggle the sidebar with <kbd className="font-sans font-medium text-foreground">⌘B</kbd> or{" "}
                <kbd className="font-sans font-medium text-foreground">Ctrl B</kbd>.
              </p>
            </div>
          </div>
        </div>
      </SidebarInset>
    </SidebarProvider>
  );
}
```

## Related

- [Sheet](https://ui.fewtone.com/components/sheet.md): An accessible React sheet for Next.js: a side panel that slides in from any edge of the screen, for forms, filters and navigation, built on Radix Dialog.
- [Tooltip](https://ui.fewtone.com/components/tooltip.md): An accessible React tooltip for Next.js and Tailwind CSS: a short hint on hover and keyboard focus, fast across a toolbar, built on Radix Tooltip.
- [Tabs](https://ui.fewtone.com/components/tabs.md): Accessible React tabs for Next.js, with an underline or pill that slides to the active tab and panels that glide in, built on Radix Tabs.
