# Pages

Measures the document once and lays it out as paginated sheets.

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

`Pages` is the one component that actually paginates: it renders the tree
it's given once, off-screen, to measure it, then renders that same tree
again, once per page, showing on each only the keeps the plan assigned to
it. Everything underneath — every `Field`, `Section`, and `Table` — is
unaware it might be rendering more than once.

## Preview


```tsx
import { Document } from "@/components/asterdoc/document";
import { Field } from "@/components/asterdoc/field";
import { Pages } from "@/components/asterdoc/pages";
import { Section } from "@/components/asterdoc/section";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function PagesDemo() {
  return (
    <Pages>
      <Document artifact={proposalForm} data={shortProposalData} id="pages-demo">
        <Section id="customer" title="Prepared for" className="flex flex-col gap-1">
          <Field path="customer" label={false} />
          <Field path="customerAddress" label={false} />
        </Section>
      </Document>
    </Pages>
  );
}
```


## Installation


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


```bash
asterdoc add pages
```


Installs to components/asterdoc/pages.tsx.


## Usage


```tsx
import { Pages } from "@/components/asterdoc/pages";

<Pages className="w-full overflow-hidden bg-neutral-100 p-10">
  <Document artifact={proposalForm} data={shortProposalData}>
    <Field path="provider" />
  </Document>
</Pages>
```

| Prop | Type | Optional | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | Yes |  | Classes for the outer frame that scales and scrolls the paginated stack. |
| `onPaginate` | `(plan: PagePlan) => void` | Yes |  | Called with the measured page plan whenever pagination changes. |
| `furniture` | `PageFurniture` | Yes |  | What every sheet carries outside the flow: a header, a footer, a stamp.
Drawn inside the document's margin, so the page count does not change. |
| `children` | `ReactNode` | No |  | The document tree measured once and paginated into sheets. |


## Composition

`Pages` wraps a [`Document`](/components/document), or a
[`Bundle`](/components/bundle) of several, and draws each page it produces
on its own [`Paper`](/components/paper) sheet, sized to the document's own
page geometry tokens. Every [`Keep Together`](/components/keep-together)
beneath it — a `Field`, a titled `Section`'s heading, each of a `Table`'s
rows — is what the plan actually places page by page; `Pages` moves nothing
itself, it only decides which keeps belong on which page.

`onPaginate` is called with the measured plan whenever pagination changes,
which is how a caller — the docs preview included — reads the page count or
drives a PDF render from the same plan the browser laid out. A `Table` whose
rows continue past a page break gets its header copied onto the continuing
page from that same plan; the copy carries `data-keep-repeat` so it's never
confused with the header's one real place in the flow.

Unlike [`Paper`](/components/paper) alone, which draws one sheet and lets
content run past its bottom, `Pages` is what keeps a document's rendered
pages matching what a reader would expect from a printed or exported copy.

### Page furniture

`furniture` is what every sheet carries outside the flow: a `header` band, a
`footer` band, and a `stamp` drawn across the whole sheet behind the content.
Each slot is composed content, and
[`PageNumber`](/components/page-number) is the one that prints the sheet it is
on. The PDF render from [`@asterdoc/react-pdf`](/sdk/react-pdf) takes the same
object.

```tsx
import { renderPdf } from "@asterdoc/react-pdf";
import { PageNumber } from "@/components/asterdoc/page-number";
import { Pages } from "@/components/asterdoc/pages";

const furniture = {
  header: <span>Northwind Partners LLP</span>,
  footer: <PageNumber />,
  stamp: <span className="text-6xl text-neutral-200">DRAFT</span>,
};

<Pages furniture={furniture}>{document}</Pages>;
// and, on the server, the same object:
await renderPdf(document, { plan, furniture });
```

A band is drawn inside the margin the document already declares, between the
paper's edge and the content box, so the content budget, the page plan and the
page count are exactly what they are without it: adding a footer never
repaginates a document. The price is a height limit. A band taller than the
margin leaves it fails by name, in the preview and in the PDF, saying which slot, how tall it laid
out, and how much margin it had; widen the document's `marginPx` token or
shorten the band.

The stamp is drawn across the whole sheet, centred on it and behind the
content, so it takes no room in the margin. Its limit is the sheet itself: a
stamp that lays out taller than the paper, or with a word or a
`whitespace-nowrap` line wider than it, fails by name rather than being cut off
at the paper's edge on every page. It is measured before any rotation.

Furniture is drawn around the document rather than inside it, so it cannot
read a field path. Pass what it prints in as props.

Each engine declares which slots it draws, and both engines draw all three.
The experimental Chromium adapter prints the header and the footer through its
own print templates and repeats the stamp on every page; it cannot print a page
number in the stamp, and refuses one by name. An engine that does not draw a
slot fails naming the engine and the slot rather than writing a document whose
every page is missing its header.

## Variants

`Pages` takes an optional `className`, `onPaginate`, and `furniture`.

### Multi-page overflow

A longer proposal's line items run past three pages, so `Pages` lays the
same one tree out across several sheets and repeats the table's header on
every page it continues onto.


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

export function PagesVariantMultiPage() {
  return (
    <Pages>
      <Document artifact={proposalForm} data={overflowProposalData} id="pages-variant-multi-page">
        <Section id="line-items" title="Scope and pricing" className="flex flex-col gap-3">
          <Table
            path="lineItems"
            id="line-items"
            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" },
            ]}
          />
        </Section>
      </Document>
    </Pages>
  );
}
```


### Page furniture

`furniture` gives every sheet a running head, a numbered foot, and a stamp
behind the content, all drawn inside the margin, so the page count is the one
the same document has without them.


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

const provider = String((overflowProposalData.fields.provider as { name?: unknown }).name ?? "");

export function PagesVariantPageFurniture() {
  return (
    <Pages
      furniture={{
        header: <span className="text-xs text-neutral-500">{provider}</span>,
        footer: <PageNumber />,
        stamp: <span className="text-6xl font-semibold text-neutral-200">DRAFT</span>,
      }}
    >
      <Document artifact={proposalForm} data={overflowProposalData} id="pages-variant-page-furniture">
        <Section id="line-items" title="Scope and pricing" className="flex flex-col gap-3">
          <Table
            path="lineItems"
            id="line-items"
            columns={[
              { field: "description", width: "basis-1/2" },
              { field: "amount", width: "basis-1/6", align: "right" },
            ]}
          />
        </Section>
      </Document>
    </Pages>
  );
}
```


### Draft watermark

A `stamp` of large, light text turned with `-rotate-45` says DRAFT across
every sheet, rising from the bottom left. Rotation is in the verified class
vocabulary, so both PDF engines draw the stamp turned and centred as the
preview does.


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

export function PagesVariantDraftWatermark() {
  return (
    <Pages
      furniture={{
        stamp: <span className="-rotate-45 text-9xl font-bold text-neutral-200">DRAFT</span>,
      }}
    >
      <Document artifact={proposalForm} data={overflowProposalData} id="pages-variant-draft-watermark">
        <Section id="line-items" title="Scope and pricing" className="flex flex-col gap-3">
          <Table
            path="lineItems"
            id="line-items"
            columns={[
              { field: "description", width: "basis-1/2" },
              { field: "amount", width: "basis-1/6", align: "right" },
            ]}
          />
        </Section>
      </Document>
    </Pages>
  );
}
```


### Custom frame

A custom `className` replaces the default scroll frame's background and
padding.


```tsx
import { Document } from "@/components/asterdoc/document";
import { Field } from "@/components/asterdoc/field";
import { Pages } from "@/components/asterdoc/pages";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function PagesVariantCustomFrame() {
  return (
    <Pages className="w-full overflow-hidden bg-neutral-100 p-10">
      <Document artifact={proposalForm} data={shortProposalData} id="pages-variant-custom-frame">
        <Field path="provider" />
      </Document>
    </Pages>
  );
}
```


### Page count

`onPaginate` reports the measured plan; here it's used to show the resulting
page count beside the preview.


```tsx
import { useState } from "react";

/**
 * The `/components/pages` docs page's "Page count" variant: `onPaginate` is
 * called with the measured plan whenever pagination changes, here used to
 * show the resulting page count beside the preview.
 */

import { Document } from "@/components/asterdoc/document";
import { Field } from "@/components/asterdoc/field";
import { Pages } from "@/components/asterdoc/pages";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function PagesVariantPageCount() {
  const [pageCount, setPageCount] = useState<number | null>(null);
  return (
    <div className="flex flex-col gap-2">
      {pageCount !== null ? (
        <p className="text-xs text-neutral-500">{pageCount} page{pageCount === 1 ? "" : "s"}</p>
      ) : null}
      <Pages onPaginate={(plan) => setPageCount(plan.pages.length)}>
        <Document artifact={proposalForm} data={shortProposalData} id="pages-variant-page-count">
          <Field path="provider" />
        </Document>
      </Pages>
    </div>
  );
}
```
