Takumi

Reports & statements

Generate PDF reports and statements with chapters, bookmarks, tables of contents, and repeating headers.

Use page breaks for chapters, repeating headers for context, and bookmarks or a table of contents for navigation.

Chapters

break-before: page starts a section on a fresh page. break-inside: avoid keeps a figure with its caption:

Start chapters on new pages
import {  } from "takumi-pdf";

const  = await (
  <>
    < ={{ : "page" }}>
      <>Results</>
      < ={{ : "avoid" }}>
        < ={} ="Revenue by quarter" ={{ : 560, : 360 }} />
        <>Revenue by quarter</>
      </>
    </>
  </>,
);

widows and orphans keep a paragraph from splitting into single lines, but they do not keep a heading with the text under it. To do that, wrap the heading and its first paragraph in a break-inside: avoid box.

See Pagination for every break property.

Bookmarks

outline: true turns h1–h6 into PDF bookmarks, nested by heading depth. Readers jump between chapters from the viewer's sidebar:

Generate chapter bookmarks
import {  } from "takumi-pdf";

const  = await (, {
  : true,
  : "en",
  : { : "Annual report 2026", : ["Acme Inc."] },
});

For a table of contents, link each chapter by its id. Here chapters is your list of { id, title } entries, and <TargetPageNumber /> fills in the page each link points to:

Build a table of contents
import {  } from "takumi-pdf/primitives";

< ="flex flex-col gap-1 text-sm">
  {.(() => (
    < ={.} ={`#${.}`} ="flex items-baseline gap-2">
      <>{.}</>
      < ="flex-1 border-b border-dotted border-gray-300" />
      < ="w-8 shrink-0 text-right" />
    </>
  ))}
</>;

Each entry stays clickable, so the contents page works on screen as well as in print. Give the number a fixed width, or a two-digit page can rewrap the entry and renumber it. See Table of contents.

Running bands

A report header usually names the document and the period, and the footer carries the page counter:

Repeat a report header
import {  } from "takumi-pdf";
import { ,  } from "takumi-pdf/primitives";

const  = await (, {
  : (
    < ="flex w-full justify-between px-12 text-[10px] text-gray-500">
      <>Annual report 2026</>
      <>Acme Inc.</>
    </>
  ),
  : (
    < ="flex w-full justify-center text-[10px] text-gray-500">
      < /> / < />
    </>
  ),
  : { : 64, : 64, : 48, : 48 },
});

Bands draw in the margin, so a numeric margin has to fit them. Leave margin unset to fit them automatically, or see Sizing the margin.

Long tables

Use <table> markup. Rows break between pages, and columns keep the same positions on every page. A <thead> that qualifies repeats at the top of each continuation page, and a <tfoot> below the last row of each page the table breaks across:

Repeat table headers
import "takumi-pdf";

< ="w-full text-xs">
  <>
    <>
      < ="text-left">Item</>
      < ="text-right">Amount</>
    </>
  </>
  <>
    {.(() => (
      < ={.}>
        <>{.}</>
        < ="text-right">{.}</>
      </>
    ))}
  </>
</>;

Tagged output maps the markup to Table, TR, TH, and TD structure elements, so screen readers navigate by row and column. See Tables for layout coverage and PDF/A and PDF/UA for the structure tree.

Batches

A statement run renders the same template many times. Create one PdfRenderer and reuse it, so each font loads once for the whole batch. Here accounts holds your records, Statement is your template, and save(id, pdf) stores each result:

Render a batch of statements
import {  } from "takumi-pdf";
import {  } from "@takumi-rs/helpers";

const  = new ();
const  = await (["Inter"]);

for (const  of ) {
  const  = await .(< ={} />, {  });
  await (., );
}

Accessibility

Set tagged: "ua1" to validate against PDF/UA-1. Supply lang and metadata.title, then review heading order, tables, and chart descriptions yourself. See PDF/A and PDF/UA.

Last updated on