# Inbox

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

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

## Code

```tsx
import * as React from "react";
import {
  AppShell,
  Avatar,
  Badge,
  Button,
  FileRow,
  IconButton,
  ListRow,
  PageHeader,
  Panel,
  SearchField,
  Stagger,
  StatTile,
  Tabs,
} from "@twentyfirstmedia/pro";
import type { ListRowProps } from "@twentyfirstmedia/pro";

function Msg(p: {
  who: string;
  time: string;
  d: number;
  me?: boolean;
  children?: React.ReactNode;
}) {
  return (
    <div
      className="tf-in"
      style={
        {
          "--d": p.d + "ms",
          display: "flex",
          gap: 12,
          flexDirection: p.me ? "row-reverse" : "row",
          alignItems: "flex-end",
        } as React.CSSProperties
      }
    >
      <Avatar name={p.who} size={28} />
      <div
        style={{
          maxWidth: "72%",
          padding: "12px 16px",
          borderRadius: "var(--radius-md)",
          borderBottomLeftRadius: p.me ? 18 : 6,
          borderBottomRightRadius: p.me ? 6 : 18,
          background: p.me ? "var(--primary)" : "var(--surface-sunken)",
          color: p.me ? "var(--primary-foreground)" : "var(--ink)",
          font: "var(--type-body-weight) 13px/20px var(--font-sans)",
        }}
      >
        {p.children}
        <div
          style={{
            font: "var(--type-body-weight) 11px/14px var(--font-sans)",
            opacity: 0.65,
            marginTop: 4,
          }}
        >
          {p.time}
        </div>
      </div>
    </div>
  );
}
function App() {
  const sel = React.useState(0);
  const list: ListRowProps[] = [
    {
      avatar: "Jonah Petit",
      title: "Jonah Petit",
      subtitle: "Can we move the pricing review to Thursday?",
      meta: "09:12",
      unread: true,
    },
    {
      avatar: "Northwind",
      title: "Northwind · Ida",
      subtitle: "Seat upgrade before renewal — 40 more",
      meta: "08:40",
      unread: true,
    },
    {
      icon: "bell",
      tint: "butter",
      title: "Billing",
      subtitle: "Card ending 4417 expires this month",
      meta: "Yesterday",
    },
    {
      avatar: "Sun Lee",
      title: "Sun Lee",
      subtitle: "Payout file for March is attached",
      meta: "Yesterday",
    },
    {
      avatar: "Blue Fern",
      title: "Blue Fern · Leo",
      subtitle: "Thanks, the export works now!",
      meta: "Mon",
    },
    {
      icon: "flag",
      tint: "blush",
      title: "Incident #88",
      subtitle: "Webhooks delayed in EU region",
      meta: "Sun",
    },
  ];
  return (
    <AppShell
      brand="Halo"
      tag="Admin"
      user={{ name: "Mara Ellis", email: "mara@halo.app" }}
      active="Inbox"
      dock={false}
      side={
        <Panel side title="Northwind" subtitle="Team plan · since 2023">
          <div className="tf-row">
            <Badge tone="success">Active</Badge>
            <Badge tone="info">Renews May 1</Badge>
          </div>
          <div
            className="tf-grid"
            style={
              {
                "--cols": "1fr 1fr",
                "--cols-sm": "1fr 1fr",
                gap: 12,
              } as React.CSSProperties
            }
          >
            <StatTile tint="butter" value="€1,240" label="MRR" />
            <StatTile tint="sky" value="24" label="Seats" />
          </div>
          <h3
            className="tf-card-title"
            style={{
              fontSize: 16,
            }}
          >
            Attachments
          </h3>
          <Stagger
            className="tf-col"
            style={{
              gap: 8,
            }}
            delay={300}
          >
            <FileRow name="seat-quote.pdf" meta="182 KB · PDF" kind="pdf" />
            <FileRow name="usage-q1.xlsx" meta="96 KB · Sheet" kind="sheet" />
          </Stagger>
        </Panel>
      }
    >
      <div className="tf-in">
        <PageHeader
          title="Inbox"
          subtitle="2 unread · 4 waiting on you"
          actions={[
            <SearchField key="s" placeholder="Search messages" />,
            <Button key="n" icon="edit">
              Compose
            </Button>,
          ]}
        />
      </div>
      <div
        className="tf-grid"
        style={
          {
            "--cols": "320px 1fr",
            gap: 16,
            minHeight: 0,
          } as React.CSSProperties
        }
      >
        <div className="tf-panel tf-panel-flush tf-panel-list">
          <Tabs block items={["All", "Unread", "Assigned"]} />
          <Stagger className="tf-list" delay={120}>
            {list.map((it, i) => (
              <ListRow
                key={i}
                {...it}
                active={sel[0] === i}
                onClick={() => {
                  sel[1](i);
                }}
              />
            ))}
          </Stagger>
        </div>
        <div
          className="tf-panel tf-in"
          style={
            {
              "--d": "160ms",
              gap: 16,
            } as React.CSSProperties
          }
        >
          <div
            className="tf-header"
            style={{
              alignItems: "center",
            }}
          >
            <div className="tf-row">
              <Avatar name="Jonah Petit" size={36} />
              <div>
                <div
                  className="tf-card-title"
                  style={{
                    padding: 0,
                    fontSize: 16,
                  }}
                >
                  Jonah Petit
                </div>
                <div className="tf-card-desc">Pricing page refresh</div>
              </div>
            </div>
            <div className="tf-row">
              <IconButton icon="star" label="Star" />
              <IconButton icon="inbox" label="Archive" />
              <IconButton icon="more" label="More" />
            </div>
          </div>
          <div
            className="tf-col"
            style={{
              gap: 12,
              padding: "8px 0",
            }}
          >
            <Msg who="Jonah Petit" time="09:02" d={260}>
              Morning! The comparison table is ready for a look.
            </Msg>
            <Msg who="Jonah Petit" time="09:12" d={360}>
              Can we move the pricing review to Thursday? Growth wants to join.
            </Msg>
            <Msg who="Mara Ellis" me time="09:20" d={460}>
              Thursday works. I will move the event and add Sun.
            </Msg>
          </div>
          <div
            className="tf-in"
            style={
              {
                "--d": "560ms",
                marginTop: "auto",
                display: "flex",
                gap: 8,
                alignItems: "center",
                padding: "4px 4px 4px 20px",
                borderRadius: "var(--radius-pill)",
                background: "var(--surface-sunken)",
              } as React.CSSProperties
            }
          >
            <IconButton icon="paperclip" label="Attach" variant="outline" />
            <input
              placeholder="Write a reply…"
              aria-label="Reply"
              style={{
                flex: 1,
                minWidth: 0,
                border: 0,
                background: "transparent",
                outline: "none",
                font: "var(--type-body-weight) 13px var(--font-sans)",
                color: "var(--ink)",
              }}
            />
            <Button icon="send">Send</Button>
          </div>
        </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. |

### FileRow

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `string` | Required |
| `meta` | `string` | Required |
| `kind?` | `"pdf" \| "fig" \| "sheet" \| "doc" \| "zip"` |  |
| `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. |

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

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