Takumi

Generate PDFs

Render invoices, reports, and other paged documents from JSX, HTML, and CSS with takumi-pdf, without a headless browser.

takumi-pdf is takumi's PDF output, for invoices, reports, and other paged documents. It shares one layout engine, a port of Chromium's layout, with image output and the paint tree. A template lays out the same way in each.

Pass JSX, an HTML string, or a node tree, and style it with CSS or Tailwind. The renderer runs in WebAssembly and does not launch a browser.

npm i takumi-pdf @takumi-rs/helpers

Render a document

Save this as invoice.tsx and run bun invoice.tsx to write invoice.pdf. On Node.js, compile the JSX first.

invoice.tsx
import {  } from "takumi-pdf";
import { ,  } from "takumi-pdf/primitives";
import {  } from "@takumi-rs/helpers";
import {  } from "node:fs/promises";

const  = await (
  <>
    <>Invoice 1042</>
    <>Total: $1,290</>
  </>,
  {
    : "a4",
    : await (["Inter"]),
    : (
      < ="flex w-full justify-center text-[10px] text-gray-500">
        Page < /> of < />
      </>
    ),
  },
);

await ("invoice.pdf", );

render() returns a Uint8Array. By default:

  • Content flows across A4 pages with automatic margins.
  • Text stays selectable, and fonts are embedded as subsets.
  • Text inside an SVG image is drawn as outlines, so it cannot be selected.

An HTML string goes through the same parser as takumi-js. A <style> tag in the string applies only to that render:

Render HTML
import {  } from "takumi-pdf";

const  = await (`
  <style>.total { font-weight: 700 }</style>
  <div><span class="total">Total: $1,290</span></div>
`);

Headers, footers, and measure() take the same inputs.

Page setup

Set page geometry
import {  } from "takumi-pdf";

const  = await (, {
  : "letter", // a page keyword, or { width, height } in CSS px at 96 dpi
  : true,
  : { : 48, : 32, : 48, : 32 },
});
OptionTypeDefaultDescription
sizea page keyword or { width, height }"a4"Page size in CSS px at 96 dpi. Keywords ignore case.
landscapebooleanfalseSwaps page width and height, including explicit sizes.
marginnumber, "auto", or { top?, right?, bottom?, left? }"auto"Margin in CSS px. A side left out of the object is "auto".
backgroundColorCSS colorunsetFills the whole page, margins included, under everything. Viewers show unset as white.

Set page geometry with these options. CSS @page rules are not parsed.

Each keyword is a portrait paper size:

KeywordSizeKeywordSize
"a3"297 × 420 mm"jis-b4"257 × 364 mm
"a4"210 × 297 mm"jis-b5"182 × 257 mm
"a5"148 × 210 mm"ledger"11 × 17 in
"b4"250 × 353 mm"legal"8.5 × 14 in
"b5"176 × 250 mm"letter"8.5 × 11 in

Automatic margins

Side"auto" resolves to
Top, bottomThe header or footer height plus a 20px inset, and at least 37.8px (1cm)
Left, right37.8px

On a page one inch or smaller along an axis, automatic margins on that axis are 0. To set a numeric margin that fits a band, see Sizing the margin.

Reuse a renderer

render() and measure() share one renderer. Create a PdfRenderer to keep a separate font registry:

Reuse a font registry
import {  } from "takumi-pdf";

const  = new ();
await .("https://example.com/fonts/Inter-Regular.woff2");

const  = await .();

Replace the example URL with your font file. A font loads once, however many renders list it.

MethodEffect
render(node, options?)Renders to PDF bytes
measure(node, options?)Returns the laid-out size in CSS px
registerFont(font)Registers a font that stays until you remove it
unregisterFont(family)Removes every face of a family
clearFonts()Removes every registered font except the bundled fallback
free()Releases the renderer's WebAssembly memory

new PdfRenderer({ fontCacheMaxBytes }) caps the decoded fonts that renders' fonts lists keep. Past the cap, the least recently used families are released and load again when a render lists them. Fonts from registerFont() are never released.

Runtimes

Use the default import on Node.js, Bun, and Cloudflare Workers. For manual initialization, import takumi-pdf/no-init.

Next.js

Import takumi-pdf/next in a Next.js route handler. It works on the Node and Edge runtimes without a serverExternalPackages entry.

app/api/pdf/route.ts
import { render } from "takumi-pdf/next";

In the browser

In a browser bundle, initialize the renderer with takumi-pdf/no-init and takumi-pdf/wasm-url:

Initialize in the browser
import init, { render } from "takumi-pdf/no-init";
import wasmUrl from "takumi-pdf/wasm-url";

await init({ module_or_path: wasmUrl });

takumi-pdf/wasm-url resolves the binary with new URL(specifier, import.meta.url). Vite, webpack, and Turbopack rewrite that call to the asset they emit. The module has no top-level await, so it also works in a worker bundled as an IIFE.

Keep the default entry in server code. A Vite SSR build leaves the URL pointing at the server chunk, where the asset is not emitted.

On Turbopack, remove any turbopack.rules entry that maps *.wasm to type: "wasm". That rule makes Turbopack instantiate the binary and look for wasm-bindgen glue the package does not ship.

Choose the next guide

TaskGuide
Control page breaks and repeated table headersPagination
Add page numbersHeaders and footers
Load fonts, logos, and photosFonts and images
Add links or a table of contentsLinks, outline, and metadata
Create a receipt or labelSingle-page viewport
Generate archival or accessible outputPDF/A and PDF/UA
Embed invoice XML or another fileAttachments

For complete document patterns, see Invoices, Reports, and Charts.

Last updated on