# Customers

> A complete screen built from Avatar, Badge, Drawer, IconButton and 9 more components.

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

## Code

```tsx
import * as React from "react";
import {
  Avatar,
  Badge,
  Drawer,
  ListRow,
  PageHeader,
  SearchField,
  Stagger,
  StatTile,
  Table,
  Tabs,
} from "@twentyfirstmedia/pro";
import { Shell } from "./_workspace";
import type { Tone } from "@twentyfirstmedia/pro";

const CO: [string, string, string, number, string][] = [
  ["Northwind", "Logistics · Hamburg", "Team", 1240, "Good"],
  ["Kestrel Bank", "Banking · Frankfurt", "Business", 3880, "Good"],
  ["Copperline", "Manufacturing · Munich", "Starter", 190, "Watch"],
  ["Harbor & Co", "Retail · Cologne", "Team", 1468, "At risk"],
  ["Lumen Labs", "Research · Vienna", "Business", 4076, "Good"],
  ["Orbit Foods", "Food · Zurich", "Team", 1445, "Good"],
];
const HT: Record<string, Tone> = {
  Good: "success",
  Watch: "warning",
  "At risk": "danger",
};
function App() {
  return (
    <Shell
      area="Customers"
      current="companies"
      action={{
        label: "New deal",
        icon: "plus",
      }}
      pages={[
        {
          id: "companies",
          label: "Companies",
          icon: "grid",
          count: 1,
        },
        {
          id: "pipeline",
          label: "Pipeline",
          icon: "trend",
          count: 2,
        },
        {
          id: "contacts",
          label: "Contacts",
          icon: "user",
        },
      ]}
      overlay={
        <Drawer
          inline
          title="Northwind"
          subtitle="Logistics · Hamburg · customer since 2021"
          avatar="Northwind"
          onClose={() => {}}
        >
          <div className="tf-row">
            <Badge tone="success">Good</Badge>
            <Badge tone="info" dot={false}>
              Team · 24 seats
            </Badge>
            <Badge dot={false}>Owner: Mara Ellis</Badge>
          </div>
          <Tabs block items={["Overview", "Invoices", "Mail", "Agent brief"]} />
          <div
            className="tf-grid"
            style={
              {
                "--cols": "1fr 1fr",
                "--cols-sm": "1fr 1fr",
              } as React.CSSProperties
            }
          >
            <StatTile tint="butter" value="€1,240" label="MRR" />
            <StatTile tint="mint" value="€14,880" label="Paid to date" />
          </div>
          <ListRow
            avatar="Ida Nolan"
            title="Ida Nolan"
            subtitle="Head of Ops · ida@northwind.example"
          />
          <ListRow
            avatar="Amara Silva"
            title="Amara Silva"
            subtitle="IT admin · amara@northwind.example"
          />
        </Drawer>
      }
    >
      <div className="tf-in">
        <PageHeader
          title="Companies"
          subtitle="15 customers · example data"
          actions={[<SearchField key="s" placeholder="Search companies" />]}
        />
      </div>
      <Stagger
        className="tf-grid"
        style={
          {
            "--cols": "repeat(4, minmax(0, 1fr))",
            "--cols-sm": "repeat(2, minmax(0, 1fr))",
            "--cols-md": "repeat(2, minmax(0, 1fr))",
          } as React.CSSProperties
        }
        delay={80}
      >
        <StatTile tint="butter" value="€33,067" label="Monthly recurring revenue" />
        <StatTile tint="sky" value="15" label="Customers" />
        <StatTile tint="placeholder" value="5" label="Need attention" />
        <StatTile tint="mint" value="€182,600" label="Open pipeline" />
      </Stagger>
      <div
        className="tf-panel tf-in"
        style={
          {
            padding: 12,
            "--d": "260ms",
          } as React.CSSProperties
        }
      >
        <Table
          columns={[
            {
              key: "0",
              label: "Company",
              render: (r) => (
                <span
                  className="tf-row"
                  style={{
                    flexWrap: "nowrap",
                  }}
                >
                  <Avatar name={r[0]} size={30} />
                  <span>
                    <b>{r[0]}</b>
                    <div className="tf-card-desc">{r[1]}</div>
                  </span>
                </span>
              ),
            },
            {
              key: "2",
              label: "Plan",
              render: (r) => (
                <Badge
                  tone={r[2] === "Business" ? "highlight" : r[2] === "Team" ? "info" : "neutral"}
                  dot={false}
                >
                  {r[2]}
                </Badge>
              ),
            },
            {
              key: "3",
              label: "MRR",
              align: "right",
              render: (r) => "€" + r[3].toLocaleString("en-US"),
            },
            {
              key: "4",
              label: "Health",
              render: (r) => <Badge tone={HT[r[4]]}>{r[4]}</Badge>,
            },
          ]}
          rows={CO}
        />
      </div>
    </Shell>
  );
}
export default function Preview() {
  return <App />;
}
```

## Props

### Avatar

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `string` | Required |
| `src?` | `string` |  |
| `size?` | `number` |  |
| `tint?` | `Tint \| "surface-sunken"` |  |
| `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. |

### Drawer

| Prop | Type | Notes |
| --- | --- | --- |
| `title` | `string` | Required |
| `subtitle?` | `ReactNode` |  |
| `avatar?` | `string` |  |
| `onClose?` | `() => void` |  |
| `width?` | `number \| string` |  |
| `inline?` | `boolean` |  |
| `children?` | `ReactNode` |  |
| `open?` | `boolean` | Optional: with `open` the component stays mounted while it animates out; without it, render it only while open. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Icon

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `IconName` | Required |
| `size?` | `number` |  |
| `label?` | `string` |  |
| `className?` | `string` |  |
| `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. |

### ListRow

| Prop | Type | Notes |
| --- | --- | --- |
| `avatar?` | `string` |  |
| `icon?` | `IconName` |  |
| `tint?` | `Tint \| "plain"` |  |
| `leading?` | `ReactNode` |  |
| `title` | `ReactNode` | Required |
| `subtitle?` | `ReactNode` |  |
| `meta?` | `ReactNode` |  |
| `trailing?` | `ReactNode` |  |
| `unread?` | `boolean` |  |
| `active?` | `boolean` |  |
| `onClick?` | `() => void` |  |
| `href?` | `string` | Makes the row a link (with `linkAs` for a router's link). |
| `linkAs?` | `ElementType` |  |
| `actions?` | `ReactNode` | Buttons of the row's own (rename, remove …), outside the clickable area so nothing interactive is nested. Without `onClick` and `href` the row is not interactive at all. |
| `titleLines?` | `number` | Lines the title may take before it ends in an ellipsis (default: it wraps freely). |
| `subtitleLines?` | `number` | Lines the subtitle may take (default 1); badges and facts wrap within them. |
| `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. |

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

### SubNav

| Prop | Type | Notes |
| --- | --- | --- |
| `eyebrow?` | `string` |  |
| `action?` | `{ label: string; icon?: IconName; onClick?: () => void }` |  |
| `items` | `SubNavItem[]` | Required |
| `current?` | `string` |  |
| `onPick?` | `(id: string) => void` |  |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Table

| Prop | Type | Notes |
| --- | --- | --- |
| `columns` | `TableColumn[]` | Required |
| `rows` | `Record<string, any>[]` | Required |
| `animate?` | `boolean` |  |
| `stack?` | `boolean` | Cards instead of columns when the table's box is narrow (up to 600px): every row stacks its cells, each with its column label. Without it narrow tables scroll sideways. |
| `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. |

### TopBar

| Prop | Type | Notes |
| --- | --- | --- |
| `brand?` | `ReactNode` | The product name, or a node (a logo mark with the name). |
| `tag?` | `string` |  |
| `items` | `NavItem[]` | Required |
| `active?` | `string` |  |
| `user?` | `{ name: string; email?: string }` |  |
| `onNavigate?` | `(label: string) => void` |  |
| `extra?` | `ReactNode` |  |
| `userOpen?` | `boolean` |  |
| `onUser?` | `() => void` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
