# Dashboard

> A complete screen built from AppShell, Badge, Button, Card and 10 more components.

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

## Code

```tsx
import * as React from "react";
import {
  AppShell,
  Badge,
  Button,
  Card,
  Chip,
  EventCard,
  IconButton,
  PageHeader,
  Panel,
  SearchField,
  Stagger,
  StatTile,
  Tabs,
  TintIcon,
} from "@twentyfirstmedia/pro";
import { cardActs } from "./_workspace";

function App() {
  const f = React.useState("All");
  return (
    <AppShell
      brand="Halo"
      tag="Admin"
      user={{ name: "Mara Ellis", email: "mara@halo.app" }}
      active="Projects"
      side={
        <Panel
          side
          title="Schedule"
          subtitle="5 items waiting · 2 today"
          action={<IconButton icon="plus" label="Add event" />}
        >
          <Tabs block items={["Events", "Approvals", "History"]} />
          <Stagger className="tf-col" delay={300}>
            <EventCard kind="meeting" label="Team sync" date="Tue, Apr 14" time="10:30">
              Roadmap check-in with design and growth; bring last sprint numbers.
            </EventCard>
            <EventCard kind="review" label="Design review" date="Wed, Apr 15" time="14:00">
              Walk through the new billing settings flow.
            </EventCard>
            <EventCard kind="deadline" label="Deadline" date="Fri, Apr 17" time="17:00">
              Send Q2 invoices to all annual customers.
            </EventCard>
          </Stagger>
        </Panel>
      }
    >
      <div className="tf-in">
        <PageHeader
          title="Projects"
          subtitle="Plan, ship and retire work across the workspace"
          actions={[
            <SearchField key="s" placeholder="Search people, projects, invoices" shortcut="⌘K" />,
          ]}
        />
      </div>
      <Stagger
        className="tf-grid"
        style={
          {
            "--cols": "repeat(4, 1fr)",
            "--cols-sm": "repeat(2, minmax(0, 1fr))",
            "--cols-md": "repeat(2, minmax(0, 1fr))",
          } as React.CSSProperties
        }
        delay={80}
      >
        <StatTile tint="sky" value="18" label="All projects" />
        <StatTile tint="mint" value="11" label="Live" delta="+2 this week" />
        <StatTile tint="placeholder" value="4" label="Drafts" />
        <StatTile tint="butter" value="€48,210" label="Revenue (MTD)" />
      </Stagger>
      <div
        className="tf-in"
        style={
          {
            "--d": "260ms",
            display: "flex",
            flexWrap: "wrap",
            gap: 12,
            alignItems: "center",
            justifyContent: "space-between",
          } as React.CSSProperties
        }
      >
        <div className="tf-row">
          {["All", "Live", "Draft", "Archived"].map((t) => (
            <Chip
              key={t}
              selected={f[0] === t}
              onClick={() => {
                f[1](t);
              }}
            >
              {t}
            </Chip>
          ))}
        </div>
        <Button icon="plus">New project</Button>
      </div>
      <Stagger
        className="tf-grid"
        style={
          {
            "--cols": "repeat(3, 1fr)",
          } as React.CSSProperties
        }
        delay={320}
      >
        <Card
          onClick={() => {}}
          title="Quarterly onboarding"
          description="Welcome flow for new workspace admins."
          meta={["Mara Ellis", "12 steps", "348 users"]}
          corner={<TintIcon icon="book" tint="mint" />}
          status={<Badge tone="success">Live</Badge>}
          actions={cardActs()}
        />
        <Card
          onClick={() => {}}
          tint="lilac"
          title="Pricing page refresh"
          description="New tiers and a comparison table for annual plans."
          meta={["Jonah Petit", "6 sections"]}
          corner={<IconButton icon="play" label="Preview" size="sm" variant="outline" />}
          status={<Badge tone="neutral">86 viewing</Badge>}
          actions={[<IconButton key={1} icon="edit" label="Edit" size="sm" variant="solid" />]}
        />
        <Card
          onClick={() => {}}
          title="Partner referral program"
          description="Track invites, payouts and partner tiers."
          meta={["Sun Lee", "4 steps", "57 partners"]}
          corner={<TintIcon icon="users" tint="sky" />}
          status={<Badge tone="success">Live</Badge>}
          actions={cardActs()}
        />
        <Card
          onClick={() => {}}
          title="Invoice #2041 is overdue"
          description="Net-30 passed four days ago; the reminder bounced."
          meta={["Rui Costa", "High priority"]}
          corner={<TintIcon icon="mail" tint="blush" />}
          status={<Badge tone="warning">Open</Badge>}
          actions={[
            <Button key={1} size="sm" variant="soft">
              Reply
            </Button>,
          ]}
        />
        <Card
          onClick={() => {}}
          title="Help center migration"
          description="Move 140 articles to the new docs layout."
          meta={["Ava Brandt", "0 published"]}
          corner={<TintIcon icon="file" tint="mint" />}
          status={<Badge>Draft</Badge>}
          actions={cardActs()}
        />
        <Card
          onClick={() => {}}
          title="Annual plan upgrade request"
          description="Northwind wants 40 more seats before renewal."
          meta={["Ida Nolan", "Normal priority"]}
          corner={<TintIcon icon="card" tint="butter" />}
          status={<Badge tone="info">Pending</Badge>}
          actions={cardActs()}
        />
      </Stagger>
    </AppShell>
  );
}
export default function Preview() {
  return <App />;
}
```

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

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

### Chip

| Prop | Type | Notes |
| --- | --- | --- |
| `selected?` | `boolean` |  |
| `icon?` | `IconName` |  |
| `count?` | `number` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### EventCard

| Prop | Type | Notes |
| --- | --- | --- |
| `kind?` | `"meeting" \| "deadline" \| "review" \| "launch" \| "incident"` |  |
| `label` | `string` | Required |
| `date` | `string` | Required |
| `time` | `string` | Required |
| `tint?` | `Tint` |  |
| `icon?` | `IconName` |  |
| `actions?` | `boolean` |  |
| `children?` | `ReactNode` |  |
| `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. |

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

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

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

### TintIcon

| Prop | Type | Notes |
| --- | --- | --- |
| `icon` | `IconName` | Required |
| `tint?` | `Tint \| "plain"` |  |
| `label?` | `string` |  |
| `size?` | `number` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
