# Sidebar

> A React sidebar for admin apps: grouped links with icons, nested sections, badges, a sliding active pill, an icon rail and a filter.

- Type: React component (Navigation)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add sidebar`
- Docs: https://wingo-ui.com/components/sidebar

## Interaction

Click a link and the active pill slides to it on a smooth spring; parents fold their children open with a chevron (a parent with its own page gets a separate chevron button), and collapsible group labels fold whole groups. Cmd/Ctrl+B or the footer button collapses it to a 64px icon rail: labels turn into tooltips, badges into dots, and parents open their children as a menu to the side. With the filter on, typing hides what does not match, opens the sections that do and bolds the matched letters; Escape clears it and arrow-down jumps into the list. An item's shortcut (Home has one in the demo) opens its page from anywhere. With the keyboard, Up / Down move between rows, Home / End jump, Right opens a parent (or steps into it) and Left closes it (or steps back to the parent). Rows darken while pressed and are 44px on touch screens; inside an app shell's phone drawer (mode="sheet") the rows grow and tapping a link closes the drawer. Collapsed and open sections are remembered with persistKey.

## Installation

```bash
npx wingo-ui@latest add sidebar
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/sidebar` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/sidebar.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Kbd](https://wingo-ui.com/components/kbd) ([Markdown](https://wingo-ui.com/components/kbd.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [usePersistedState](https://wingo-ui.com/components/use-persisted-state) ([Markdown](https://wingo-ui.com/components/use-persisted-state.md))
- [useScrollEdges](https://wingo-ui.com/components/use-scroll-edges) ([Markdown](https://wingo-ui.com/components/use-scroll-edges.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))

## Usage

```tsx
import { ChartColumn, FileText, Handshake, Inbox, LayoutDashboard, LifeBuoy, Package, Settings, ShoppingCart, Users } from "lucide-react"
import { Sidebar, type SidebarNavGroup } from "@/components/ui/sidebar"

const groups: SidebarNavGroup[] = [
  {
    items: [
      { label: "Home", href: "/", icon: <LayoutDashboard /> },
      { label: "Inbox", href: "/inbox", icon: <Inbox />, badge: 4 },
    ],
  },
  {
    label: "Sales",
    items: [
      { label: "Clients", href: "/clients", icon: <Users /> },
      { label: "Offers", href: "/offers", icon: <Handshake />, badge: 12 },
      {
        label: "Invoices",
        href: "/invoices",
        icon: <FileText />,
        items: [
          { label: "Issued", href: "/invoices/issued" },
          { label: "Paid", href: "/invoices/paid" },
          { label: "Overdue", href: "/invoices/overdue", badge: 3 },
        ],
      },
    ],
  },
  {
    label: "Operations",
    collapsible: true,
    items: [
      { label: "Products", href: "/products", icon: <Package /> },
      { label: "Orders", href: "/orders", icon: <ShoppingCart /> },
      { label: "Reports", href: "/reports", icon: <ChartColumn /> },
    ],
  },
  {
    label: "Account",
    items: [
      { label: "Settings", href: "/settings", icon: <Settings /> },
      { label: "Help", href: "https://example.com/help", icon: <LifeBuoy /> },
    ],
  },
]

export function AppSidebar({ pathname }: { pathname: string }) {
  return (
    <Sidebar
      groups={groups}
      currentPath={pathname}
      persistKey="crm-sidebar"
      filter
      user={{ name: "Emma Carter", email: "emma.carter@northwind.com", avatar: "/demo/ana-pop.jpg", plan: "Pro" }}
      className="h-dvh"
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `groups?` | `SidebarNavGroup[]` |  | The navigation: { id?, label?, items, collapsible?, defaultOpen? } per group. Items are NavItem ({ label, href, icon, activeIcon, badge, shortcut, keywords, external, disabled, match, onSelect, items }), nested one level deep. |
| `items?` | `SidebarNavItem[]` |  | One unlabeled group, for small apps. Ignored when groups is set. |
| `header?` | `ReactNode` |  | Above the navigation: a WorkspaceSwitcher variant="card" or a logo. It adapts to the rail through the group/sidebar CSS contract (data-collapsed="true"). |
| `footer?` | `ReactNode` |  | Below the navigation, before the collapse button: a UserMenu trigger="card". Replaces the user card. |
| `user?` | `{ name: string; email?: string; avatar?: string; plan?: ReactNode }` |  | A simple user card in the footer when there is no footer; onUserClick makes it a button. |
| `onUserClick?` | `(event: MouseEvent<HTMLButtonElement>) => void` |  | Makes the user card a button. |
| `defaultCollapsed?` | `boolean` | `false` | Starts as the 64px icon rail. Use collapsed + onCollapsedChange to control it; inside an AppShell the shell owns it. |
| `collapsed?` | `boolean` |  | Controlled collapsed state. |
| `onCollapsedChange?` | `(collapsed: boolean) => void` |  | Fires when the rail opens or closes (button, shortcut). |
| `value?` | `string` |  | The active item's key (its id, else href, else label): controlled. Without it the active item follows currentPath, else the sidebar keeps its own (defaultValue, default the first enabled item). |
| `defaultValue?` | `string` |  | The first active item when neither value nor a path is known. |
| `onValueChange?` | `(key: string, item: SidebarNavItem) => void` |  | Fires on every activation, in all three modes. |
| `currentPath?` | `string` | `""` | The path the active item is matched against (the longest matching href wins). Default: the LinkProvider's. The playground's pretend router provides it. |
| `persistKey?` | `string` | `""` | Remembers collapsed (standalone) and the open sections in local storage under `${persistKey}-collapsed` and `${persistKey}-open`. Empty keeps them in memory. |
| `loading?` | `boolean` | `false` | Skeleton rows (icon + line) in place of the items, with aria-busy on the nav. |
| `filter?` | `boolean` | `false` | A filter field above the navigation. Matches labels and keywords ignoring case and diacritics, opens the sections that hold a match and bolds the matched letters. On the rail it becomes a search button that expands the sidebar. |
| `filterPlaceholder?` | `string` | `"Search"` | The filter's placeholder. |
| `filterShortcut?` | `string` | `""` | A key combo (mod+k notation) that focuses the filter from anywhere, expanding the rail first. Shown as a hint in the field. |
| `defaultFilterValue?` | `string` | `""` | Starts the filter with a query (uncontrolled), e.g. a search carried over from another page. |
| `onFilterValueChange?` | `(value: string) => void` |  | Fires as the filter text changes. |
| `filterValue?` | `string` |  | The filter text, controlled. |
| `variant?` | `"sidebar" \| "floating" \| "inset"` | `"sidebar"` | sidebar: flush, a hairline on the end edge. floating: a raised rounded panel 8px off the edges. inset: no edge and no fill, on the app shell's tint. |
| `activeStyle?` | `"soft" \| "raised" \| "bar"` | `"soft"` | The active row: a tinted pill, a raised white pill (best on floating or tinted sidebars), or a 3px bar on the start edge over a light tint. |
| `size?` | `"sm" \| "md"` | `"md"` | Rows 32 / 36px with 13 / 14px text; 44px on touch screens either way. |
| `width?` | `number` | `256` | Expanded width in px (160 to 480). The rail is always 64px; the width animates between the two. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Colors the active indicator, the active row's text and its badge. Neutral is a gray pill with black / white text. |
| `color?` | `string` | `""` | Any CSS color for the active indicator; replaces the tone. |
| `collapsible?` | `"icon" \| "none"` | `"icon"` | icon: it can become the 64px rail (button and shortcut). none: always expanded, no toggle. |
| `showToggle?` | `boolean` | `true` | The collapse button at the end of the footer, with a tooltip showing the shortcut. |
| `shortcut?` | `string` | `"mod+b"` | Toggles the rail from anywhere when the sidebar stands alone (mod is Cmd on Apple devices, Ctrl elsewhere). Inside an app shell the shell binds its own. Empty turns it off. |
| `itemShortcuts?` | `boolean` | `true` | Binds each item's shortcut (mod+k notation, shown as a hint on hover and in rail tooltips) to open that page from anywhere, with aria-keyshortcuts on the row. Off leaves them as hints. Sheet mode never binds them (the desktop copy does). |
| `tooltips?` | `boolean` | `true` | On the rail every row shows its label (and shortcut) in a tooltip to the side; expanded, only labels cut off by the width do. |
| `flyouts?` | `boolean` | `true` | On the rail a parent opens its children as a menu to the side (a bottom sheet on phones). Off, clicking a parent expands the sidebar with that parent open. |
| `mode?` | `"rail" \| "sheet"` | `"rail"` | rail: the desktop column, collapsible. sheet: the content of a phone drawer: never collapsed, 44px rows at 15px, no toggle, and a tap on a link closes the app shell's drawer (the app shell renders its drawer copy this way). |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` |  | The router link for this sidebar; default the LinkProvider's, else a plain <a>. |
| `labels?` | `{ sidebar?; navigation?; filter?; noResults?: (query) => string; collapse?; expand?; toggleGroup?: (label) => string; clearFilter?; loading? }` |  | Every string, English by default: "Sidebar", "Main", "Filter navigation", "Nothing matches “…”", "Collapse sidebar", "Expand sidebar", "Toggle …", "Clear filter", "Loading navigation". |
| `classNames?` | `Partial<Record<"inner" \| "header" \| "filter" \| "content" \| "group" \| "groupLabel" \| "list" \| "item" \| "link" \| "icon" \| "label" \| "badge" \| "indicator" \| "sublist" \| "empty" \| "footer" \| "toggle" \| "userCard", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the <aside>. Set --sidebar-bg to change its surface. |
| `children?` | `ReactNode` |  | Replaces the generated groups inside the scrolling nav (SidebarGroup / SidebarItem). |

## Parts

### SidebarRoot (component)

The <aside> with the state (collapsed, active item, open sections, filter) for the parts below. Takes every Sidebar prop except header, footer, user, filter* and showToggle. Props: SidebarRootProps.

### SidebarHeader (component)

The top area (p-3); a hairline appears under it when the list scrolls. Takes div props.

### SidebarFooter (component)

The bottom area; a hairline appears while more of the list is below, and it stacks vertically on the rail. Takes div props.

### SidebarContent (component)

The scrolling <nav> with fading edges and the arrow-key navigation. Renders the root's groups when it has no children. Takes nav props.

### SidebarGroup (component)

One labelled group (a fold-away label when collapsible), for hand-built layouts. Props: { group: SidebarNavGroup; index?: number; children?: ReactNode }.

### SidebarItem (component)

One row with its nested children. Props: { item: SidebarNavItem; index?: number; depth?: number; parentKey?: string }.

### SidebarFilter (component)

The filter field (a search button on the rail). Props: { placeholder?: string; shortcut?: string } & input props.

### SidebarToggle (component)

The collapse button with its tooltip; renders nothing in sheet mode or when collapsible is none. Props: ButtonProps.

### SidebarUserCard (component)

Avatar, name, plan and email; the avatar alone on the rail. A button with onClick or interactive (a menu trigger). Props: { user: SidebarUser; trailing?: ReactNode; interactive?: boolean } & button props.

### useSidebar (hook)

The sidebar's state for anything rendered inside it. Signature: () => { collapsed; setCollapsed; toggle; mode; activePath }.

### SidebarNavItem (type)

One link, the shared NavItem: id, label, textValue, href, icon, badge, description, disabled, external, match ("exact" | "prefix"), onSelect, nested items, keywords for the filter and shortcut.

### SidebarNavGroup (type)

A group of items under an optional label: id, label, items, collapsible (the label folds the group) and defaultOpen.

### SidebarUser (type)

The footer user card: name, email, avatar and plan.

### UseSidebarResult (type)

What useSidebar returns: collapsed, setCollapsed, toggle, mode and activePath (the active item with its parents, or null).

## Examples

- **The icon rail**: defaultCollapsed: tooltips to the side, dots for badges, and Invoices and Settings open their pages as a flyout menu.
- **Filter**: “rest” finds Invoices › Overdue: the section opens and the match is bold. Press F to focus it, Escape to clear.
- **Floating with a raised pill**: variant="floating" + activeStyle="raised" on a tinted page, the white pill sliding between rows.
- **Bar indicator in the brand tone**: activeStyle="bar" + tone="brand": a 3px bar on the start edge that runs along the guide line into nested rows.
- **Phone drawer (sheet mode)**: mode="sheet" inside a drawer: 44px rows at 15px, no toggle, and tapping a link closes the drawer.
- **Loading**: loading renders skeleton rows (icon + line) with aria-busy.
- **External links and “New” badges**: Any node works as a badge; external links get a trailing arrow and a new tab.

## See also

- [App Shell](https://wingo-ui.com/components/app-shell) ([Markdown](https://wingo-ui.com/components/app-shell.md))
- [Hook Sidebar](https://wingo-ui.com/components/hook-sidebar) ([Markdown](https://wingo-ui.com/components/hook-sidebar.md))
- [Bounce Sidebar](https://wingo-ui.com/components/bounce-sidebar) ([Markdown](https://wingo-ui.com/components/bounce-sidebar.md))
- [Topbar](https://wingo-ui.com/components/topbar) ([Markdown](https://wingo-ui.com/components/topbar.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))

---

Canonical: https://wingo-ui.com/components/sidebar
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
