# Sidebar Shell

> The app shell with a labelled sidebar instead of the icon rail: AppShell navigation="sidebar".

Pattern · Pro kit · [Live page](https://ui.twentyfirst.media/pro/patterns/SidebarShellScreen/)

The app shell with a labelled sidebar instead of the icon rail: `AppShell navigation="sidebar"`.

- The sidebar sits on the dark frame, the page on the light sheet, with the side panel and dock as
  in every other shell.
- `sidebarGroups` takes the same groups as `Sidebar`; without it, `nav` becomes one group.
- `workspaces` puts a `WorkspaceSwitcher` at the top, `user` and `userMenu` a `UserMenu` at the
  foot; both turn compact when the sidebar collapses. Further `Sidebar` props go in `sidebar`
  (`storageKey`, `shortcut`, `linkAs`).
- On phones the sidebar becomes a drawer behind a menu button above the page.

## Code

```tsx
import * as React from "react";
import {
  AppShell,
  Badge,
  Button,
  IconButton,
  KeyValue,
  PageHeader,
  Panel,
  Progress,
  SearchField,
  Stagger,
  StatTile,
} from "@twentyfirstmedia/pro";
import type { SidebarGroup } from "@twentyfirstmedia/pro";

const groups: SidebarGroup[] = [
  {
    items: [
      { id: "overview", label: "Overview", icon: "home" },
      { id: "inbox", label: "Inbox", icon: "inbox", count: 12 },
      { id: "calendar", label: "Calendar", icon: "calendar" },
    ],
  },
  {
    label: "Workspace",
    action: { label: "New project", icon: "plus" },
    items: [
      { id: "projects", label: "Projects", icon: "grid", count: 3 },
      { id: "customers", label: "Customers", icon: "users" },
      {
        id: "finance",
        label: "Finance",
        icon: "card",
        items: [
          { id: "invoices", label: "Invoices", icon: "file" },
          { id: "payouts", label: "Payouts", icon: "download" },
          { id: "tax", label: "Tax", icon: "chart" },
        ],
      },
      { id: "reports", label: "Reports", icon: "chart", badge: "New" },
    ],
  },
  {
    label: "Settings",
    collapsible: true,
    items: [
      { id: "team", label: "Team", icon: "users" },
      { id: "integrations", label: "Integrations", icon: "webhook", badge: "dot" },
      { id: "security", label: "Security", icon: "shield" },
    ],
  },
];

/** The app shell with a labelled sidebar on the dark frame: groups, nesting, workspace and account. */
export default function Preview() {
  const [page, setPage] = React.useState("invoices");
  const [ws, setWs] = React.useState("halo");
  return (
    <AppShell
      navigation="sidebar"
      sidebarGroups={groups}
      active={page}
      onNavigate={setPage}
      sidebar={{ shortcut: true }}
      workspaces={{
        items: [
          { id: "halo", name: "Halo Studio", sub: "Pro plan", tint: "lilac" },
          { id: "north", name: "Northwind", sub: "Team plan", tint: "mint" },
        ],
        value: ws,
        onChange: setWs,
      }}
      user={{ name: "Mara Ellis", email: "mara@halo.app" }}
      userMenu={[
        { label: "Profile", icon: "user" },
        { label: "Settings", icon: "settings" },
        "-",
        { label: "Sign out", icon: "logout" },
      ]}
      dock={false}
      side={
        <Panel side title="Payout" subtitle="Next on Friday">
          <KeyValue
            items={[
              ["Amount", "€18,420"],
              ["Invoices", "42"],
              ["Fees", "€312"],
            ]}
          />
          <Progress value={68} label="Collected" />
          <Button variant="secondary" icon="download">
            Export
          </Button>
        </Panel>
      }
    >
      <div className="tf-in">
        <PageHeader
          title="Invoices"
          subtitle="April · 42 sent, 7 open"
          actions={[
            <SearchField key="s" placeholder="Search invoices" shortcut="⌘K" />,
            <Button key="n" variant="primary" icon="plus">
              New invoice
            </Button>,
          ]}
        />
      </div>
      <Stagger
        className="tf-grid"
        style={
          {
            "--cols": "repeat(3, minmax(0, 1fr))",
            "--cols-sm": "1fr",
            gap: 16,
          } as React.CSSProperties
        }
      >
        <StatTile value="€48,210" label="Billed" tint="sky" delta="+12%" />
        <StatTile value="€6,940" label="Open" tint="butter" delta="7 invoices" />
        <StatTile value="€1,280" label="Overdue" tint="blush" delta="2 invoices" />
      </Stagger>
      <Panel
        title="Latest"
        subtitle="Sent this week"
        action={<IconButton icon="filter" label="Filter" />}
      >
        <KeyValue
          items={[
            [
              "INV-2041 · Northwind",
              <Badge key="a" tone="warning">
                Open
              </Badge>,
            ],
            [
              "INV-2040 · Kestrel Bank",
              <Badge key="b" tone="success">
                Paid
              </Badge>,
            ],
            [
              "INV-2039 · Copperline",
              <Badge key="c" tone="success">
                Paid
              </Badge>,
            ],
          ]}
        />
      </Panel>
    </AppShell>
  );
}
```

## Props

### AppShell

| Prop | Type | Notes |
| --- | --- | --- |
| `active?` | `string` |  |
| `brand?` | `ReactNode` | The product name, or a node (a logo mark with the name; collapsed sidebars show it whole). |
| `tag?` | `string` |  |
| `nav?` | `NavItem[]` |  |
| `user?` | `{ name: string; email?: string }` |  |
| `onNavigate?` | `(label: string) => void` |  |
| `topExtra?` | `ReactNode` |  |
| `sheetStyle?` | `CSSProperties` |  |
| `contentKey?` | `string \| number` |  |
| `side?` | `ReactNode` |  |
| `dock?` | `boolean` |  |
| `dockItems?` | `DockProps["items"]` |  |
| `railTop?` | `RailItem[]` |  |
| `railBottom?` | `RailItem[]` |  |
| `navigation?` | `"top" \| "sidebar"` | `top` (default): navigation in the top bar, tools in the side rail. `sidebar`: a labelled sidebar on the frame, with groups, nesting and a drawer on phones. |
| `sidebarGroups?` | `SidebarGroup[]` | With `navigation="sidebar"`: groups instead of the flat `nav` list. |
| `sidebar?` | `Partial<Omit<SidebarProps, "variant" \| "groups">>` | With `navigation="sidebar"`: more sidebar props (collapsed, storageKey, shortcut, linkAs …). |
| `workspaces?` | `WorkspaceSwitcherProps` | With `navigation="sidebar"`: the workspace switcher at the top instead of the brand. |
| `userMenu?` | `(MenuItem \| "-")[]` | The account menu's entries (with `navigation="sidebar"` at the foot of the sidebar). |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### 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. |

### 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. |

### 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. |

### Badge

| Prop | Type | Notes |
| --- | --- | --- |
| `tone?` | `Tone` |  |
| `dot?` | `boolean` |  |
| `live?` | `boolean` |  |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Button

| Prop | Type | Notes |
| --- | --- | --- |
| `variant?` | `"primary" \| "secondary" \| "soft" \| "ghost" \| "danger" \| "danger-solid"` |  |
| `size?` | `"md" \| "sm"` |  |
| `icon?` | `IconName` |  |
| `iconRight?` | `IconName` |  |
| `disabled?` | `boolean` |  |
| `type?` | `"button" \| "submit" \| "reset"` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `children?` | `ReactNode` |  |
| `asChild?` | `boolean` | Render the single child element (a link, a router Link) with the button's look instead of a <button>. |
| `id?` | `string` |  |
| `name?` | `string` | Form field name and value, sent with the form when this button submits it. |
| `value?` | `string` |  |
| `form?` | `string` | The id of the form this button belongs to, when it sits outside it. |
| `title?` | `string` |  |
| `tabIndex?` | `number` |  |
| `autoFocus?` | `boolean` |  |
| `aria-label?` | `string` |  |
| `aria-labelledby?` | `string` |  |
| `aria-describedby?` | `string` |  |
| `aria-expanded?` | `boolean \| "true" \| "false"` | For disclosure and menu triggers: whether what the button controls is open. |
| `aria-controls?` | `string` |  |
| `aria-haspopup?` | `boolean \| "true" \| "false" \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` |  |
| `aria-pressed?` | `boolean \| "true" \| "false" \| "mixed"` | For toggle buttons. |
| `aria-current?` | `boolean \| "true" \| "false" \| "page" \| "step" \| "location" \| "date" \| "time"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### IconButton

| Prop | Type | Notes |
| --- | --- | --- |
| `icon` | `IconName` | Required |
| `label` | `string` | Accessible name and tooltip (`aria-label` and `title`); `aria-label` or `title` override them. |
| `variant?` | `"soft" \| "solid" \| "outline" \| "ghost"` |  |
| `size?` | `"sm" \| "md" \| "lg"` |  |
| `type?` | `"button" \| "submit" \| "reset"` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `disabled?` | `boolean` |  |
| `asChild?` | `boolean` | Render the single child element (a link) with the button's look; the icon is placed inside it. |
| `children?` | `ReactNode` |  |
| `id?` | `string` |  |
| `name?` | `string` | Form field name and value, sent with the form when this button submits it. |
| `value?` | `string` |  |
| `form?` | `string` | The id of the form this button belongs to, when it sits outside it. |
| `title?` | `string` |  |
| `tabIndex?` | `number` |  |
| `autoFocus?` | `boolean` |  |
| `aria-label?` | `string` |  |
| `aria-labelledby?` | `string` |  |
| `aria-describedby?` | `string` |  |
| `aria-expanded?` | `boolean \| "true" \| "false"` | For disclosure and menu triggers: whether what the button controls is open. |
| `aria-controls?` | `string` |  |
| `aria-haspopup?` | `boolean \| "true" \| "false" \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` |  |
| `aria-pressed?` | `boolean \| "true" \| "false" \| "mixed"` | For toggle buttons. |
| `aria-current?` | `boolean \| "true" \| "false" \| "page" \| "step" \| "location" \| "date" \| "time"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### KeyValue

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `KeyValueItem[]` | Required |
| `dense?` | `boolean` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### PageHeader

| Prop | Type | Notes |
| --- | --- | --- |
| `crumbs?` | `(string \| Crumb)[]` | A breadcrumb above the title (see `Breadcrumb`): labels, or items with `href`/`onClick`. |
| `crumbLinkAs?` | `ElementType` | The element crumb links render as, e.g. Next.js `Link`. |
| `title` | `string` | Required |
| `subtitle?` | `string` |  |
| `actions?` | `ReactNode[]` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Panel

| Prop | Type | Notes |
| --- | --- | --- |
| `side?` | `boolean` |  |
| `title?` | `string` |  |
| `subtitle?` | `string` |  |
| `action?` | `ReactNode` |  |
| `children?` | `ReactNode` |  |
| `surface?` | `Surface` | `"glass"`: translucent glass over what is behind (maps, 3D, video, photos); `"solid"` opts out inside a glass `UIProvider`. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Progress

| Prop | Type | Notes |
| --- | --- | --- |
| `value` | `number` | Required |
| `tint?` | `"sky" \| "mint" \| "butter"` |  |
| `label?` | `string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### SearchField

| Prop | Type | Notes |
| --- | --- | --- |
| `placeholder?` | `string` |  |
| `label?` | `string` | Accessible name (default: the placeholder, or "Search"). |
| `shortcut?` | `string` | The key hint, e.g. "⌘K" (hidden on touch screens). |
| `bindShortcut?` | `boolean` | ⌘/Ctrl + the hint's last key (K for "⌘K") focuses the field, except while typing elsewhere. |
| `value?` | `string` |  |
| `defaultValue?` | `string` |  |
| `onChange?` | `(e: ChangeEvent<HTMLInputElement>) => void` |  |
| `onKeyDown?` | `(e: KeyboardEvent<HTMLInputElement>) => void` |  |
| `onFocus?` | `(e: FocusEvent<HTMLInputElement>) => void` |  |
| `onBlur?` | `(e: FocusEvent<HTMLInputElement>) => void` |  |
| `id?` | `string` |  |
| `name?` | `string` |  |
| `autoFocus?` | `boolean` |  |
| `inputProps?` | `InputHTMLAttributes<HTMLInputElement>` | For a search with suggestions (combobox): role, aria-controls, aria-activedescendant … |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Stagger

| Prop | Type | Notes |
| --- | --- | --- |
| `as?` | `string` |  |
| `className?` | `string` |  |
| `style?` | `CSSProperties` |  |
| `delay?` | `number` | Extra delay before the first child, in ms. |
| `reveal?` | `boolean` | Below the fold, wait until the group scrolls into view (default true). |
| `children?` | `ReactNode` |  |

### StatTile

| Prop | Type | Notes |
| --- | --- | --- |
| `value` | `ReactNode` | Required |
| `label` | `string` | Required |
| `tint?` | `Tint \| "surface" \| "placeholder"` |  |
| `delta?` | `string` |  |
| `delay?` | `number` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
