# People

> A complete screen built from AppShell, Avatar, Badge, Button and 11 more components.

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

## Code

```tsx
import * as React from "react";
import {
  AppShell,
  Avatar,
  Badge,
  Button,
  Chip,
  Donut,
  IconButton,
  PageHeader,
  Pagination,
  Panel,
  SearchField,
  Stagger,
  StatTile,
  Table,
  Timeline,
} from "@twentyfirstmedia/pro";

const MEMBERS = [
  {
    n: "Mara Ellis",
    e: "mara@halo.app",
    r: "Owner",
    t: "Leadership",
    s: "Active",
    tone: "success",
    a: "Now",
  },
  {
    n: "Jonah Petit",
    e: "jonah@halo.app",
    r: "Admin",
    t: "Design",
    s: "Active",
    tone: "success",
    a: "5 min ago",
  },
  {
    n: "Sun Lee",
    e: "sun@halo.app",
    r: "Editor",
    t: "Growth",
    s: "Active",
    tone: "success",
    a: "1 h ago",
  },
  {
    n: "Rui Costa",
    e: "rui@halo.app",
    r: "Editor",
    t: "Finance",
    s: "Away",
    tone: "warning",
    a: "Yesterday",
  },
  {
    n: "Ava Brandt",
    e: "ava@halo.app",
    r: "Viewer",
    t: "Support",
    s: "Invited",
    tone: "info",
    a: "—",
  },
  {
    n: "Ida Nolan",
    e: "ida@halo.app",
    r: "Editor",
    t: "Sales",
    s: "Active",
    tone: "success",
    a: "3 h ago",
  },
  {
    n: "Theo Marsh",
    e: "theo@halo.app",
    r: "Viewer",
    t: "Design",
    s: "Suspended",
    tone: "danger",
    a: "12 days ago",
  },
];
function App() {
  const f = React.useState("Everyone");
  return (
    <AppShell
      brand="Halo"
      tag="Admin"
      user={{ name: "Mara Ellis", email: "mara@halo.app" }}
      active="People"
      side={
        <Panel side title="Team" subtitle="42 seats · 38 used">
          <Donut
            size={120}
            stroke={16}
            legend={false}
            center="38"
            caption="of 42"
            data={[
              {
                label: "Design",
                value: 9,
                tint: "lilac",
              },
              {
                label: "Growth",
                value: 11,
                tint: "mint",
              },
              {
                label: "Support",
                value: 8,
                tint: "sky",
              },
              {
                label: "Finance",
                value: 4,
                tint: "butter",
              },
              {
                label: "Sales",
                value: 6,
                tint: "blush",
              },
            ]}
          />
          <div className="tf-legend">
            {[
              ["Design", "lilac"],
              ["Growth", "mint"],
              ["Support", "sky"],
              ["Finance", "butter"],
              ["Sales", "blush"],
            ].map((x) => (
              <span key={x[0]}>
                <i
                  style={{
                    background: "var(--" + x[1] + "-ink)",
                  }}
                />
                {x[0]}
              </span>
            ))}
          </div>
          <h3
            className="tf-card-title"
            style={{
              fontSize: 16,
              marginTop: 8,
            }}
          >
            Recent activity
          </h3>
          <Timeline
            delay={400}
            items={[
              {
                avatar: "Jonah Petit",
                text: (
                  <span>
                    <b>Jonah</b>
                    {" changed Ava to Viewer"}
                  </span>
                ),
                time: "10 min ago",
              },
              {
                icon: "send",
                tint: "sky",
                text: "3 invites sent to support@",
                time: "1 h ago",
              },
              {
                avatar: "Mara Ellis",
                text: (
                  <span>
                    <b>Mara</b>
                    {" suspended Theo Marsh"}
                  </span>
                ),
                time: "Mon",
              },
            ]}
          />
        </Panel>
      }
    >
      <div className="tf-in">
        <PageHeader
          title="People"
          subtitle="Everyone with access to this workspace"
          actions={[
            <SearchField key="s" placeholder="Search by name or email" />,
            <Button key="b" icon="users">
              Invite
            </Button>,
          ]}
        />
      </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="38" label="Members" />
        <StatTile tint="mint" value="31" label="Active today" />
        <StatTile tint="placeholder" value="3" label="Pending invites" />
        <StatTile tint="lilac" value="6" label="Admins" />
      </Stagger>
      <div
        className="tf-in tf-row"
        style={
          {
            "--d": "240ms",
          } as React.CSSProperties
        }
      >
        {["Everyone", "Admins", "Editors", "Viewers", "Invited"].map((t) => (
          <Chip
            key={t}
            selected={f[0] === t}
            onClick={() => {
              f[1](t);
            }}
          >
            {t}
          </Chip>
        ))}
      </div>
      <div
        className="tf-panel tf-panel-flush tf-in"
        style={
          {
            "--d": "300ms",
          } as React.CSSProperties
        }
      >
        <Table
          columns={[
            {
              key: "n",
              label: "Name",
              render: (r) => (
                <span
                  className="tf-row"
                  style={{
                    flexWrap: "nowrap",
                  }}
                >
                  <Avatar name={r.n} size={32} />
                  <span>
                    <div
                      style={{
                        fontWeight: "var(--type-title-weight)",
                      }}
                    >
                      {r.n}
                    </div>
                    <div
                      style={{
                        fontSize: 12,
                        color: "var(--ink-muted)",
                      }}
                    >
                      {r.e}
                    </div>
                  </span>
                </span>
              ),
            },
            {
              key: "r",
              label: "Role",
            },
            {
              key: "t",
              label: "Team",
            },
            {
              key: "s",
              label: "Status",
              render: (r) => <Badge tone={r.tone}>{r.s}</Badge>,
            },
            {
              key: "a",
              label: "Last active",
            },
            {
              key: "x",
              label: "",
              align: "right",
              render: () => <IconButton icon="more" label="Member options" size="sm" />,
            },
          ]}
          rows={MEMBERS}
        />
        <div
          className="tf-row"
          style={{
            justifyContent: "space-between",
            padding: "8px 16px",
          }}
        >
          <span className="tf-card-desc">Showing 7 of 38</span>
          <Pagination total={6} page={1} />
        </div>
      </div>
    </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. |

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

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

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

### Donut

| Prop | Type | Notes |
| --- | --- | --- |
| `data` | `{ label: string; value: number; tint?: Tint \| "ink" }[]` | Required |
| `center?` | `string` |  |
| `caption?` | `string` |  |
| `size?` | `number` |  |
| `stroke?` | `number` |  |
| `legend?` | `boolean` |  |
| `label?` | `string` |  |
| `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. |

### Pagination

| Prop | Type | Notes |
| --- | --- | --- |
| `total` | `number` | Number of pages. |
| `page?` | `number` | The current page (1-based). Controlled together with `onChange`; without `onChange` it is only the start page, as in earlier versions (prefer `defaultPage` for that). |
| `defaultPage?` | `number` | Start page when uncontrolled (default 1). |
| `onChange?` | `(page: number) => void` | A page was picked (previous, next or a number); receives the new 1-based page. |
| `href?` | `(page: number) => string` | Link mode for server-rendered lists: the address of a page; pages become links. |
| `linkAs?` | `ElementType` | The element links render as (default `a`), e.g. Next.js `Link`. |
| `label?` | `string` | Accessible name of the navigation (default "Pagination"), e.g. "Search results pages". |
| `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. |

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

### Timeline

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `{ avatar?: string; icon?: IconName; tint?: Tint \| "plain"; text: ReactNode; time: string; }[]` | Required |
| `delay?` | `number` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
