> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openpdf.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Tables

> Real table markup gets you column tracks, repeated headers across pages, and break-aware rows.

Real `<table>` markup is the only correct shape for tabular data. The engine gives it column tracks (header and body cells align without manual width math), a repeated `<thead>` on every page a long table spans, and break-aware row layout. A flex-and-div imitation gets none of that.

## Canonical pattern

```tsx theme={null}
const items = [
  { sku: 'SVC-101', name: 'Discovery workshop', qty: 2, unit: 1850 },
  // ...
];

const money = (n: number) =>
  `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;

<table tw="w-full border border-slate-300 text-[10.5px]">
  <thead>
    <tr tw="bg-slate-100 font-bold">
      <th tw="border-b border-slate-300 p-2 text-left">SKU</th>
      <th tw="border-b border-slate-300 p-2 text-left">Description</th>
      <th tw="border-b border-slate-300 p-2 text-right">Qty</th>
      <th tw="border-b border-slate-300 p-2 text-right">Amount</th>
    </tr>
  </thead>
  <tbody>
    {items.map((it) => (
      <tr key={it.sku} tw="border-b border-slate-200">
        <td tw="p-2 align-top">{it.sku}</td>
        <td tw="p-2 align-top">{it.name}</td>
        <td tw="p-2 text-right align-top">{String(it.qty)}</td>
        <td tw="p-2 text-right align-top">{money(it.qty * it.unit)}</td>
      </tr>
    ))}
  </tbody>
</table>
```

Data lives in a typed const at the top of the file; row JSX lives in the `.map` body. This is the data-rows shape from [Documents](/authoring/documents#data-rows-vs-designed-repeats).

## Rules

* **Numeric columns right-align** (`text-right`), including their `<th>`. Money gets fixed decimals via `toLocaleString` so digits line up.
* `align-top` on cells when any column can wrap to two lines.
* Two-line cells (title plus muted detail) are a nested `<div tw="flex flex-col">` inside the `<td>`. Keep the detail line short.
* Keep rows short: one title line plus one detail line. A row that can't fit twice on a page is a layout smell.
* Tables are for scannable data. Paragraphs of text belong in body copy, not cells.

## Totals

Totals do **not** go in the table. A totals row inside `<tbody>` can strand alone on the next page. Close the table, then render a right-aligned summary block. `breakInside: 'avoid'` is reliable on that block:

```tsx theme={null}
<div tw="mt-6 flex justify-end" style={{ breakInside: 'avoid' }}>
  <div tw="flex w-[260px] flex-col text-[11px]">
    <div tw="flex justify-between py-1">
      <span tw="text-slate-500">Subtotal</span>
      <span>{money(subtotal)}</span>
    </div>
    <div tw="flex justify-between py-2 text-[13px] font-bold text-slate-900">
      <span>Total due</span>
      <span>{money(total)}</span>
    </div>
  </div>
</div>
```

## Engine quirks

<Warning>
  Two known engine behaviors to design around, not fight:

  * **No explicit widths on `<th>`.** `tw="w-[45%]"` on a header cell leaves an unpainted notch in the header row's background fill. Column tracks size themselves from content; steer them by controlling cell content, not header widths. A hairline seam can still appear at a column boundary even without widths. It is a cosmetic upstream paint quirk; don't fight it with markup.
  * **`breakInside: 'avoid'` on `<tr>` is unreliable** when the table sits inside a flex ancestor. A tall row can still split across pages. Keep row content short instead of building keep-together logic around rows. On ordinary block elements the declaration is reliable, see [Pagination](/authoring/pagination).
</Warning>

## Anti-patterns

* Flex rows pretending to be a table. Headers drift from body columns and nothing repeats across pages.
* `w-[...]` on `<th>`, or hand-balancing column widths at all.
* Left-aligned numbers, or floating-precision money (`$49.9`).
* A totals row inside `<tbody>`.
* Giant cells with paragraphs of text.
