# Invoice

> A complete screen built from Button, IconButton, InvoicePreview, PageHeader and 5 more components.

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

## Code

```tsx
import * as React from "react";
import {
  Button,
  InvoicePreview,
  PageHeader,
  Select,
  TintIcon,
  TransactionRow,
} from "@twentyfirstmedia/pro";
import { Shell } from "./_workspace";

function App() {
  const lines = [
    {
      d: "Halo Team plan · 40 seats · May 2026",
      q: 40,
      u: 24,
      v: 19,
    },
    {
      d: "Onboarding workshop (half day)",
      q: 1,
      u: 650,
      v: 19,
    },
  ];
  return (
    <Shell
      area="Finance"
      current="invoices"
      action={{
        label: "New invoice",
        icon: "plus",
      }}
      pages={[
        {
          id: "overview",
          label: "Overview",
          icon: "chart",
        },
        {
          id: "invoices",
          label: "Invoices",
          icon: "card",
          count: 4,
        },
        {
          id: "expenses",
          label: "Expenses",
          icon: "file",
          count: 4,
        },
        {
          id: "bank",
          label: "Bank",
          icon: "swap",
          count: 4,
        },
        {
          id: "vat",
          label: "VAT return",
          icon: "flag",
        },
        {
          id: "pnl",
          label: "Profit & loss",
          icon: "trend",
        },
      ]}
    >
      <div className="tf-in">
        <PageHeader
          crumbs={["Finance", "Invoices", "New"]}
          title="Invoice #2097"
          subtitle="Totals update as you type"
          actions={[
            <Button key="d" variant="secondary">
              Save draft
            </Button>,
            <Button key="s" icon="send">
              {"Create & send"}
            </Button>,
          ]}
        />
      </div>
      <div
        className="tf-grid"
        style={
          {
            "--cols": "minmax(0, 1.1fr) minmax(0, 1fr)",
            gap: 16,
            alignItems: "start",
          } as React.CSSProperties
        }
      >
        <div
          className="tf-panel tf-in"
          style={
            {
              "--d": "80ms",
            } as React.CSSProperties
          }
        >
          <div
            className="tf-grid"
            style={
              {
                "--cols": "1fr 1fr",
              } as React.CSSProperties
            }
          >
            <Select label="Customer" options={["Northwind", "Kestrel Bank"]} />
            <Select label="Payment terms" options={["14 days", "30 days"]} />
          </div>
          {lines.map((l, i) => (
            <div key={i} className="tf-file">
              <TintIcon icon="card" tint="butter" />
              <div
                style={{
                  flex: 1,
                  minWidth: 0,
                }}
              >
                <div className="tf-file-name">{l.d}</div>
                <div className="tf-file-meta">{l.q + " × €" + l.u + " · VAT " + l.v + "%"}</div>
              </div>
              <b>{"€" + (l.q * l.u).toLocaleString("en-US") + ".00"}</b>
            </div>
          ))}
          <Button variant="soft" icon="plus">
            Add line
          </Button>
        </div>
        <div
          className="tf-in"
          style={
            {
              "--d": "160ms",
            } as React.CSSProperties
          }
        >
          <InvoicePreview
            brand="Halo"
            number={2097}
            from="Halo Workspace GmbH · Example Street 1 · 20457 Hamburg"
            to="Northwind"
            issued="Apr 16, 2026"
            due="Apr 30, 2026"
            lines={lines}
            note="Thank you for your business. Please pay within 14 days."
          />
        </div>
      </div>
      <div
        className="tf-col tf-in"
        style={
          {
            gap: 8,
            "--d": "240ms",
          } as React.CSSProperties
        }
      >
        <h3
          className="tf-card-title"
          style={{
            padding: 0,
          }}
        >
          Bank: to match
        </h3>
        <TransactionRow
          text="STRIPE PAYOUT 0415"
          date="Apr 15, 2026"
          amount={2318.4}
          hint="Payout for 3 card payments: invoices #2093, #2094, #2095"
        />
      </div>
    </Shell>
  );
}
export default function Preview() {
  return <App />;
}
```

## Props

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

### InvoicePreview

| Prop | Type | Notes |
| --- | --- | --- |
| `number` | `string \| number` | Required |
| `from?` | `string` |  |
| `to` | `string` | Required |
| `issued` | `string` | Required |
| `due` | `string` | Required |
| `lines` | `{ d: string; q: number; u: number; v: number }[]` | Required |
| `note?` | `string` |  |
| `kind?` | `string` |  |
| `brand?` | `string` |  |
| `format?` | `(n: number) => string` |  |
| `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. |

### Select

| Prop | Type | Notes |
| --- | --- | --- |
| `options` | `string[]` | Required |
| `value?` | `string` |  |
| `defaultValue?` | `string` |  |
| `onChange?` | `(e: ChangeEvent<HTMLSelectElement>) => void` |  |
| `label?` | `string` |  |
| `hint?` | `string` |  |
| `error?` | `string` |  |
| `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. |

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

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

### TransactionRow

| Prop | Type | Notes |
| --- | --- | --- |
| `text` | `string` | Required |
| `date` | `string` | Required |
| `amount` | `number` | Required |
| `matched?` | `boolean` |  |
| `matchRef?` | `string` |  |
| `hint?` | `string` |  |
| `onMatch?` | `() => void` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
