# Sidebar

> App navigation down the side: groups with headings, counts and badges, one level of nested pages, links, item menus and a collapsed mode with icons only.

Component · Navigation · Halo kit · [Live page](https://ui.twentyfirst.media/halo/components/Sidebar/)

App navigation down the side: groups with headings, counts and badges, one level of nested pages,
links, item menus and a collapsed mode with icons only.

- Three looks: `variant="floating"` (default, a card with a gap to the edge), `"attached"` (flush
  with the left edge, full height, a hairline to the content) and `"shell"` (on the dark frame;
  `AppShell navigation="sidebar"` uses it).
- Items with `href` render as links; pass your router's link as `linkAs` (Next.js `Link`, React
  Router `Link`). `external` opens in a new tab and says so to screen readers.
- Extras per item: `count`, `badge` (`"dot"` for new activity or a short word like `"New"`),
  `disabled`, `actions` (a "…" menu on hover and focus) and `items` for nested pages. Groups take
  `collapsible`, `defaultOpen` and an `action` such as "New project".
- Collapsed, every label is a tooltip; a parent opens a flyout with its pages on hover, click or
  →. Pass `storageKey` to remember the state and `shortcut` for ⌘B / Ctrl+B.
- `header` and `footer` take nodes or a function of `{ collapsed }`, e.g. a `WorkspaceSwitcher`
  and a `UserMenu` that turn compact.
- On phones (≤ 760px) the sidebar becomes an off-canvas drawer behind a menu button: Escape and the
  scrim close it, focus stays inside and returns to the button. Control it with `mobileOpen` and
  `onMobileOpenChange`.
- For filters, use `as="aside"`: items with `checked` become checkbox (or, with the group's
  `select: "single"`, radio) options with counts, and a group's `content` holds sliders or date
  fields. See the Filter Sidebar pattern.

## Example

```tsx
import * as React from "react";
import { Sidebar, UserMenu, WorkspaceSwitcher } from "@twentyfirstmedia/halo";
import type { MenuItem, SidebarGroup } from "@twentyfirstmedia/halo";

const itemMenu: (MenuItem | "-")[] = [
  { label: "Rename", icon: "edit" },
  { label: "Share", icon: "share" },
  "-",
  { label: "Archive", icon: "archive", danger: true },
];

const groups: SidebarGroup[] = [
  {
    items: [
      { id: "home", label: "Home", icon: "home" },
      { id: "inbox", label: "Inbox", icon: "mail", count: 4 },
      { id: "activity", label: "Activity", icon: "bell", badge: "dot" },
    ],
  },
  {
    label: "Projects",
    action: { label: "New project", icon: "plus" },
    items: [
      { id: "launch", label: "Spring launch", icon: "sparkle", actions: itemMenu },
      { id: "pricing", label: "Pricing page", icon: "tag", badge: "New", actions: itemMenu },
    ],
  },
  {
    label: "Finance",
    collapsible: true,
    items: [
      {
        id: "billing",
        label: "Billing",
        icon: "card",
        items: [
          { id: "invoices", label: "Invoices", icon: "file" },
          { id: "payouts", label: "Payouts", icon: "download" },
        ],
      },
      { id: "reports", label: "Reports", icon: "chart" },
    ],
  },
  {
    label: "Help",
    collapsible: true,
    items: [
      { id: "docs", label: "Docs", icon: "book", href: "#docs", external: true },
      { id: "api", label: "API access", icon: "code", disabled: true },
    ],
  },
];

const workspaces = [
  { id: "halo", name: "Halo Studio", sub: "Pro plan", tint: "lilac" as const },
  { id: "north", name: "Northwind", sub: "Team plan", tint: "mint" as const },
];

const account: (MenuItem | "-")[] = [
  { label: "Profile", icon: "user" },
  { label: "Settings", icon: "settings" },
  "-",
  { label: "Sign out", icon: "logout" },
];

export default function Preview() {
  const [page, setPage] = React.useState("launch");
  const [ws, setWs] = React.useState("halo");
  const parts = {
    groups,
    current: page,
    onPick: setPage,
    header: ({ collapsed }: { collapsed: boolean }) => (
      <WorkspaceSwitcher items={workspaces} value={ws} onChange={setWs} compact={collapsed} />
    ),
    footer: ({ collapsed }: { collapsed: boolean }) => (
      <UserMenu name="Mara Ellis" email="mara@halo.app" items={account} compact={collapsed} />
    ),
  };
  return (
    <div className="tf-row" style={{ gap: 24, alignItems: "stretch", flexWrap: "wrap" }}>
      <div style={{ height: 600, display: "flex" }}>
        <Sidebar label="Workspace" shortcut {...parts} />
      </div>
      <div style={{ height: 600, display: "flex" }}>
        <Sidebar label="Workspace (collapsed)" defaultCollapsed {...parts} />
      </div>
    </div>
  );
}
```

## Props

### Sidebar

| Prop | Type | Notes |
| --- | --- | --- |
| `groups` | `SidebarGroup[]` | Required |
| `current?` | `string` | Id (or label) of the current page. |
| `onPick?` | `(id: string) => void` |  |
| `variant?` | `"floating" \| "attached" \| "shell"` | `floating` (default): a card with rounded corners and a shadow. `attached`: full height, flush with the edge of the screen, divided by a line. `shell`: on the dark frame of `AppShell`. |
| `collapsed?` | `boolean` | Icons only. |
| `defaultCollapsed?` | `boolean` |  |
| `onCollapsedChange?` | `(collapsed: boolean) => void` |  |
| `storageKey?` | `string` | Remember the collapsed state in the browser under this key. |
| `shortcut?` | `boolean` | ⌘B (Ctrl+B) collapses and expands the sidebar, except while typing in a field. |
| `linkAs?` | `ElementType` | The element items with an `href` render as (default `a`), e.g. Next.js `Link`. |
| `mobileOpen?` | `boolean` | On phones (up to 760px wide): whether the drawer is open (controlled). |
| `onMobileOpenChange?` | `(open: boolean) => void` |  |
| `header?` | `ReactNode \| ((state: { collapsed: boolean }) => ReactNode)` | Top of the sidebar (brand, `WorkspaceSwitcher`); a function gets the collapsed state. |
| `footer?` | `ReactNode \| ((state: { collapsed: boolean }) => ReactNode)` | Bottom of the sidebar (`UserMenu`, settings); a function gets the collapsed state. |
| `as?` | `"nav" \| "aside"` | `nav` (default) for app navigation. `aside` for filters, settings or other tools beside the content: a complementary region instead of a navigation landmark. |
| `collapsible?` | `boolean` | Offers the collapse button and the icons-only mode (default true; filters usually set false). |
| `label?` | `string` | Accessible name of the navigation (or the aside). |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### UserMenu

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `string` | Required |
| `email?` | `string` |  |
| `avatar?` | `string` | Picture URL; initials otherwise. |
| `items?` | `(MenuItem \| "-")[]` | The menu: profile, settings, sign out … |
| `compact?` | `boolean` | Only the avatar (a collapsed sidebar). |
| `side?` | `"top" \| "bottom"` | Where the menu opens (default `top`, for the foot of a sidebar). |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### WorkspaceSwitcher

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `{ id: string; name: string; sub?: string; tint?: Tint; logo?: ReactNode }[]` | Required |
| `value?` | `string` |  |
| `onChange?` | `(id: string) => void` |  |
| `label?` | `string` |  |
| `footer?` | `ReactNode` |  |
| `variant?` | `"frame"` |  |
| `width?` | `number` |  |
| `compact?` | `boolean` | Only the workspace's mark, its name as the accessible name (a collapsed sidebar). |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Menu

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `(MenuItem \| "-")[]` | Required |
| `label?` | `string` | Accessible name of the menu. |
| `autoFocus?` | `boolean` | Focus the first item on mount (menus opened from a button or the keyboard). |
| `onClose?` | `() => void` | Called after an item runs, and on Escape, so the owner can close the menu. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
