# Billing

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

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

## Code

```tsx
import * as React from "react";
import {
  AppShell,
  Avatar,
  Badge,
  BarChart,
  Button,
  Icon,
  IconButton,
  PageHeader,
  Panel,
  Progress,
  ProgressRing,
  Stagger,
  StatTile,
  Table,
} from "@twentyfirstmedia/pro";

const INV = [
  {
    id: "#2044",
    c: "Northwind",
    d: "Apr 12, 2026",
    a: "€1,240.00",
    s: "Paid",
    tone: "success",
  },
  {
    id: "#2043",
    c: "Copperline",
    d: "Apr 10, 2026",
    a: "€3,900.00",
    s: "Due",
    tone: "info",
  },
  {
    id: "#2042",
    c: "Blue Fern",
    d: "Apr 8, 2026",
    a: "€420.00",
    s: "Paid",
    tone: "success",
  },
  {
    id: "#2041",
    c: "Harbor & Co",
    d: "Mar 2, 2026",
    a: "€860.00",
    s: "Overdue",
    tone: "danger",
  },
  {
    id: "#2040",
    c: "Lumen Labs",
    d: "Feb 28, 2026",
    a: "€2,150.00",
    s: "Paid",
    tone: "success",
  },
];
function App() {
  return (
    <AppShell
      brand="Halo"
      tag="Admin"
      user={{ name: "Mara Ellis", email: "mara@halo.app" }}
      active="Billing"
      side={
        <Panel side title="Your plan" subtitle="Renews on May 1, 2026">
          <div
            className="tf-card tf-bg-lilac"
            style={{
              minHeight: 0,
            }}
          >
            <div
              className="tf-row"
              style={{
                justifyContent: "space-between",
              }}
            >
              <span
                className="tf-card-title"
                style={{
                  padding: 0,
                }}
              >
                Team
              </span>
              <Badge tone="neutral" dot={false}>
                Annual
              </Badge>
            </div>
            <div>
              <span className="tf-stat-value">€96</span>
              <span className="tf-card-desc">{" / seat / month"}</span>
            </div>
            <Button variant="primary" iconRight="arrowUpRight">
              Upgrade to Business
            </Button>
          </div>
          <div
            className="tf-row"
            style={{
              gap: 16,
              flexWrap: "nowrap",
            }}
          >
            <ProgressRing value={90} tint="butter" size={76} stroke={8} />
            <div>
              <div
                style={{
                  fontWeight: "var(--type-title-weight)",
                }}
              >
                38 of 42 seats
              </div>
              <div className="tf-card-desc">Add seats before inviting more people.</div>
            </div>
          </div>
          <h3
            className="tf-card-title"
            style={{
              fontSize: 16,
            }}
          >
            Usage
          </h3>
          <div className="tf-col">
            <div>
              <div
                className="tf-row"
                style={{
                  justifyContent: "space-between",
                  fontSize: 13,
                  fontWeight: "var(--type-label-weight)",
                  marginBottom: 8,
                }}
              >
                <span>Storage</span>
                <span>62 / 100 GB</span>
              </div>
              <Progress value={62} tint="sky" />
            </div>
            <div>
              <div
                className="tf-row"
                style={{
                  justifyContent: "space-between",
                  fontSize: 13,
                  fontWeight: "var(--type-label-weight)",
                  marginBottom: 8,
                }}
              >
                <span>API calls</span>
                <span>41k / 50k</span>
              </div>
              <Progress value={82} tint="butter" />
            </div>
          </div>
        </Panel>
      }
    >
      <div className="tf-in">
        <PageHeader
          title="Billing"
          subtitle="Invoices, payments and your subscription"
          actions={[
            <Button key="e" variant="secondary" icon="download">
              Export CSV
            </Button>,
            <Button key="n" icon="plus">
              New invoice
            </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="butter" value="€48,210" label="Collected (MTD)" />
        <StatTile tint="sky" value="€12,480" label="Outstanding" />
        <StatTile tint="blush" value="3" label="Overdue invoices" />
        <StatTile tint="mint" value="98.2%" label="On-time rate" />
      </Stagger>
      <Stagger
        className="tf-grid"
        style={
          {
            "--cols": "1fr 1fr",
          } as React.CSSProperties
        }
        delay={280}
      >
        <div
          className="tf-card"
          style={{
            minHeight: 0,
            background: "var(--frame)",
            color: "var(--on-frame)",
          }}
        >
          <div
            className="tf-row"
            style={{
              justifyContent: "space-between",
            }}
          >
            <span
              style={{
                font: "var(--type-label-weight) 12px/16px var(--font-sans)",
                color: "var(--on-frame-muted)",
              }}
            >
              Default payment method
            </span>
            <Icon name="card" size={20} />
          </div>
          <div
            style={{
              font: "var(--type-title-weight) 20px/26px var(--font-sans)",
              letterSpacing: ".08em",
            }}
          >
            •••• •••• •••• 4417
          </div>
          <div
            className="tf-row"
            style={{
              justifyContent: "space-between",
              color: "var(--on-frame-muted)",
              fontSize: 13,
            }}
          >
            <span>Mara Ellis</span>
            <span>Expires 04/26</span>
          </div>
        </div>
        <Panel title="Collected per month">
          <BarChart
            height={92}
            barWidth={22}
            format={(v) => "€" + Math.round(v / 1000) + "k"}
            data={[
              {
                label: "Nov",
                value: 31000,
              },
              {
                label: "Dec",
                value: 28000,
              },
              {
                label: "Jan",
                value: 36000,
              },
              {
                label: "Feb",
                value: 39000,
              },
              {
                label: "Mar",
                value: 44000,
              },
              {
                label: "Apr",
                value: 48210,
                tint: "butter",
              },
            ]}
          />
        </Panel>
      </Stagger>
      <div
        className="tf-panel tf-panel-flush tf-in"
        style={
          {
            "--d": "420ms",
          } as React.CSSProperties
        }
      >
        <Table
          columns={[
            {
              key: "id",
              label: "Invoice",
            },
            {
              key: "c",
              label: "Customer",
              render: (r) => (
                <span className="tf-row">
                  <Avatar name={r.c} size={26} />
                  {r.c}
                </span>
              ),
            },
            {
              key: "d",
              label: "Issued",
            },
            {
              key: "s",
              label: "Status",
              render: (r) => <Badge tone={r.tone}>{r.s}</Badge>,
            },
            {
              key: "a",
              label: "Amount",
              align: "right",
            },
            {
              key: "x",
              label: "",
              align: "right",
              render: () => <IconButton icon="download" label="Download PDF" size="sm" />,
            },
          ]}
          rows={INV}
        />
      </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. |

### BarChart

| Prop | Type | Notes |
| --- | --- | --- |
| `data` | `{ label: string; value: number; tint?: Tint \| "ink" }[]` | Required |
| `tint?` | `Tint \| "ink"` |  |
| `height?` | `number` |  |
| `width?` | `number` |  |
| `barWidth?` | `number` |  |
| `format?` | `(v: number) => string` |  |
| `label?` | `string` |  |
| `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. |

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

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

### Progress

| Prop | Type | Notes |
| --- | --- | --- |
| `value` | `number` | Required |
| `tint?` | `"sky" \| "mint" \| "butter"` |  |
| `label?` | `string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ProgressRing

| Prop | Type | Notes |
| --- | --- | --- |
| `value` | `number` | Required |
| `tint?` | `Tint \| "ink"` |  |
| `size?` | `number` |  |
| `stroke?` | `number` |  |
| `caption?` | `string` |  |
| `label?` | `string` |  |
| `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. |
