# Attached Sidebar

> A full-height app with the sidebar attached to the left edge (Sidebar variant="attached"): the calm layout for docs, settings and content tools.

Pattern · Halo kit · [Live page](https://ui.twentyfirst.media/halo/patterns/AttachedSidebarScreen/)

A full-height app with the sidebar attached to the left edge (`Sidebar variant="attached"`): the
calm layout for docs, settings and content tools.

- The sidebar fills the height and ends in a hairline; the content scrolls next to it.
- Collapse it to icons with the footer button or ⌘B; on phones it becomes a drawer.

## Code

```tsx
import * as React from "react";
import {
  Badge,
  Button,
  Card,
  PageHeader,
  Sidebar,
  Stagger,
  Tabs,
  UserMenu,
} from "@twentyfirstmedia/halo";
import type { SidebarGroup } from "@twentyfirstmedia/halo";

const groups: SidebarGroup[] = [
  {
    items: [
      { id: "home", label: "Home", icon: "home" },
      { id: "search", label: "Search", icon: "search" },
      { id: "updates", label: "Updates", icon: "bell", count: 3 },
    ],
  },
  {
    label: "Library",
    action: { label: "New collection", icon: "plus" },
    items: [
      { id: "guides", label: "Guides", icon: "book" },
      { id: "templates", label: "Templates", icon: "layers" },
      { id: "assets", label: "Assets", icon: "image" },
      { id: "archive", label: "Archive", icon: "archive" },
    ],
  },
  {
    label: "Teams",
    collapsible: true,
    items: [
      { id: "design", label: "Design", icon: "sparkle" },
      { id: "growth", label: "Growth", icon: "trend" },
    ],
  },
];

const docs = [
  ["Onboarding checklist", "Guides", "Updated today"],
  ["Brand voice", "Guides", "Updated 2 days ago"],
  ["Launch email", "Templates", "Updated last week"],
  ["Pricing table", "Templates", "Updated last week"],
];

/** A full-height app with the sidebar attached to the edge of the screen. */
export default function Preview() {
  const [page, setPage] = React.useState("guides");
  return (
    <div
      style={{
        display: "flex",
        minHeight: 640,
        overflow: "hidden",
        borderRadius: "var(--radius-lg)",
        border: "1px solid var(--line)",
        background: "var(--canvas)",
      }}
    >
      <Sidebar
        variant="attached"
        label="Library"
        groups={groups}
        current={page}
        onPick={setPage}
        header={
          <span className="tf-brand" style={{ color: "var(--ink)" }}>
            Halo Docs
          </span>
        }
        footer={({ collapsed }) => (
          <UserMenu
            name="Mara Ellis"
            email="mara@halo.app"
            compact={collapsed}
            items={[{ label: "Profile", icon: "user" }, "-", { label: "Sign out", icon: "logout" }]}
          />
        )}
      />
      <main className="tf-col" style={{ flex: 1, minWidth: 0, gap: 20, padding: 24 }}>
        <PageHeader
          title="Guides"
          subtitle="Everything the team writes down once"
          actions={[
            <Button key="n" variant="primary" icon="plus">
              New guide
            </Button>,
          ]}
        />
        <Tabs items={["All", "Drafts", "Shared with me"]} />
        <Stagger className="tf-col" style={{ gap: 12, alignContent: "start" }}>
          {docs.map(([title, kind, when]) => (
            <Card key={title} style={{ minHeight: 0 }}>
              <div className="tf-row" style={{ justifyContent: "space-between", gap: 12 }}>
                <div className="tf-col" style={{ gap: 4 }}>
                  <b>{title}</b>
                  <small className="tf-muted">{when}</small>
                </div>
                <Badge>{kind}</Badge>
              </div>
            </Card>
          ))}
        </Stagger>
      </main>
    </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. |

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

### Card

| Prop | Type | Notes |
| --- | --- | --- |
| `title?` | `string` |  |
| `description?` | `string` |  |
| `meta?` | `string[]` |  |
| `corner?` | `ReactNode` |  |
| `status?` | `ReactNode` |  |
| `actions?` | `ReactNode[]` |  |
| `tint?` | `Tint \| "surface"` |  |
| `onClick?` | `() => void` |  |
| `children?` | `ReactNode` |  |
| `eyebrow?` | `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. |

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

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

### Tabs

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `string[]` | Required |
| `value?` | `string` |  |
| `defaultValue?` | `string` |  |
| `onChange?` | `(v: string) => void` |  |
| `block?` | `boolean` |  |
| `panels?` | `Record<string, ReactNode>` | Content per tab; renders the tab panels under the bar, wired with tab/tabpanel roles. |
| `label?` | `string` | Accessible name of the tab list. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
