# Table

A list field as rows, with a header that repeats on every page it continues onto.

Canonical URL: https://docs.asterdoc.com/components/table/

`Table` is the one component whose header can outlive its own page: when a
table's rows continue past a page break, the page plan copies its header
onto the next page so the columns stay legible, and marks that copy
`data-keep-repeat` so it is never confused with the header's one real place
in the flow. A `continuedLabel` reads on that copy alone; a `footer`
evaluates rows from the artifact's own definitions and stays with the
table's last row; and a column's `render` can replace one cell's plain text
with markup of its own, without changing how the row paginates.

## Preview


```tsx
import { Document } from "@/components/asterdoc/document";
import { Table } from "@/components/asterdoc/table";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function TableDemo() {
  return (
    <Document artifact={proposalForm} data={shortProposalData} id="table-demo">
      <Table
        path="lineItems"
        id="line-items"
        columns={[
          { field: "description", width: "basis-1/2" },
          { field: "quantity", header: "Qty", width: "basis-1/12", align: "right" },
          { field: "unit", width: "basis-1/12" },
          { field: "unitPrice", header: "Unit price", width: "basis-1/6", align: "right" },
          { field: "amount", width: "basis-1/6", align: "right" },
        ]}
      />
    </Document>
  );
}
```


## Installation


```bash
npx shadcn@4 add @asterdoc/table
```


```bash
asterdoc add table
```


Installs to components/asterdoc/table.tsx.


## Usage


```tsx
import { Table } from "@/components/asterdoc/table";

<Table
  path="lineItems"
  columns={[{ field: "description" }, { field: "amount", align: "right" }]}
/>
```

| Prop | Type | Optional | Default | Description |
| --- | --- | --- | --- | --- |
| `path` | `string` | No |  | Path to the list field this table reads its rows from. |
| `columns` | `readonly TableColumn[]` | No |  | Column definitions: each names a list-item field, its header, width, and alignment. |
| `id` | `string` | Yes |  | Prefix for the header's and each row's pagination id; falls back to `path`. |
| `className` | `string` | Yes |  | Classes for the table's wrapping element. |
| `continuedLabel` | `string` | Yes |  | Shown on the repeated header when the table continues onto a later page, never on the first. Kept to one line. |
| `footer` | `readonly TableFooterRow[]` | Yes |  | Footer rows evaluated from the artifact's definitions; the page plan never opens a page on them without the table's last row. |


## Composition

A `Table` needs the same [`Document`](/components/document) context every
other component here does, and its `path` names a list field in the
artifact — the same convention `Field` uses, just pointed at an array.
Each `columns` entry
names one field of the list's items and gets a Tailwind `basis-*` share of
the row's width, so together they should sum to the whole row rather than
leave columns floating loose.

`Table` renders as flex rows, not `<table>` markup, because the default PDF
engine has no table support: a document using real table markup on screen
could not be the same tree the PDF renders. What it brings along is
[`Keep Together`](/components/keep-together) for both
the header row and every data row — each is its own pagination unit — which
is also what makes header repetition possible: the plan can place a copy of
the header keep wherever a continued row needs one.

A `Table` normally lives inside a [`Section`](/components/section) alongside
the [`Field`](/components/field)s around it, though nothing requires
that — the Preview above renders one directly inside `Document`.

The header's size follows the document's [typography](/components/typography)
token; the cells inherit the root's.

## Variants

`Table`'s columns each take a `field`, an optional `header` override, a
`width`, and an `align`.

### Compact

A two-column subset of the same `lineItems` table.


```tsx
import { Document } from "@/components/asterdoc/document";
import { Table } from "@/components/asterdoc/table";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function TableVariantCompact() {
  return (
    <Document artifact={proposalForm} data={shortProposalData} id="table-variant-compact">
      <Table
        path="lineItems"
        id="table-variant-compact"
        columns={[{ field: "description" }, { field: "amount", align: "right" }]}
      />
    </Document>
  );
}
```


### Left-aligned

Every column left-aligned, instead of the default's right-aligned numeric
ones.


```tsx
import { Document } from "@/components/asterdoc/document";
import { Table } from "@/components/asterdoc/table";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function TableVariantLeftAligned() {
  return (
    <Document artifact={proposalForm} data={shortProposalData} id="table-variant-left-aligned">
      <Table
        path="lineItems"
        id="table-variant-left-aligned"
        columns={[
          { field: "description", align: "left" },
          { field: "quantity", header: "Qty", align: "left" },
          { field: "unitPrice", header: "Rate", align: "left" },
        ]}
      />
    </Document>
  );
}
```


### Custom headers

`header` overrides the artifact's own field labels.


```tsx
import { Document } from "@/components/asterdoc/document";
import { Table } from "@/components/asterdoc/table";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function TableVariantCustomHeaders() {
  return (
    <Document artifact={proposalForm} data={shortProposalData} id="table-variant-custom-headers">
      <Table
        path="lineItems"
        id="table-variant-custom-headers"
        columns={[
          { field: "description", header: "Item" },
          { field: "amount", header: "Total", align: "right" },
        ]}
      />
    </Document>
  );
}
```


### Continued

`continuedLabel` shows on the repeated header once the table's rows carry it
past a page break, and only there — the table's own, first header never
shows it. This variant is the page's only one that self-wraps in
[`Pages`](/components/pages): every other table on this page fits one page,
which is exactly where the label has nothing to show.


```tsx
import { Document } from "@/components/asterdoc/document";
import { Pages } from "@/components/asterdoc/pages";
import { Table } from "@/components/asterdoc/table";
import { proposalForm } from "@/examples/proposal";
import { overflowProposalData } from "@/examples/proposal-data";

export function TableVariantContinued() {
  return (
    <Pages>
      <Document artifact={proposalForm} data={overflowProposalData} id="table-variant-continued">
        <Table
          path="lineItems"
          id="table-variant-continued"
          continuedLabel="(continued)"
          columns={[
            { field: "description", width: "basis-1/2" },
            { field: "quantity", header: "Qty", width: "basis-1/12", align: "right" },
            { field: "unitPrice", header: "Unit price", width: "basis-1/6", align: "right" },
            { field: "amount", width: "basis-1/6", align: "right" },
          ]}
        />
      </Document>
    </Pages>
  );
}
```


### Footer

`footer` takes rows shaped like [`Totals`](/components/totals)'s: a `def`,
an optional `label`, and `emphasis`. Each is evaluated from the artifact's
own definitions and printed as one more keep after the last row. When the
footer does not fit on the last row's page, the last row moves to the next
page with it, so the totals never print without a row above them.


```tsx
import { Document } from "@/components/asterdoc/document";
import { Table } from "@/components/asterdoc/table";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function TableVariantFooter() {
  return (
    <Document artifact={proposalForm} data={shortProposalData} id="table-variant-footer">
      <Table
        path="lineItems"
        id="table-variant-footer"
        columns={[{ field: "description" }, { field: "amount", align: "right" }]}
        footer={[{ def: "subtotal" }, { def: "total", emphasis: true }]}
      />
    </Document>
  );
}
```


### Cell renderer

A column's `render` receives the cell's already-formatted text and the row
it belongs to, and returns the markup to print instead. The row underneath
still paginates as one unit.


```tsx
import { Document } from "@/components/asterdoc/document";
import { Table } from "@/components/asterdoc/table";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function TableVariantCellRenderer() {
  return (
    <Document artifact={proposalForm} data={shortProposalData} id="table-variant-cell-renderer">
      <Table
        path="lineItems"
        id="table-variant-cell-renderer"
        columns={[
          { field: "description" },
          {
            field: "quantity",
            header: "Qty",
            align: "right",
            render: (text) => (
              <span className="rounded bg-neutral-100 px-1.5 py-0.5 text-neutral-700">{text}</span>
            ),
          },
          { field: "amount", align: "right" },
        ]}
      />
    </Document>
  );
}
```
