Takumi

Generate PDFs

Install takumi-pdf, render your first PDF from JSX or HTML, and find the guide for your document.

takumi-pdf is takumi's PDF output, for invoices, reports, and other paged documents. 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.

It shares one layout engine, a port of Chromium's layout, with image output and the paint tree. Each output applies the same layout rules to a template. To turn an HTML file into a PDF without writing code, use the command line instead.

Install

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, these hold:

  • 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.

Find your guide

TaskGuide
Control page breaks and repeated table headersPagination
Add page numbers, headers, and footersHeaders & footers
Load fonts, logos, and photosFonts & images
Add links, a table of contents, or bookmarksLinks, outline & metadata
Create a receipt, certificate, or labelSingle-page PDFs
Generate archival or accessible outputPDF/A & PDF/UA
Embed invoice XML or another fileAttachments
Build a complete documentInvoices, Reports & statements, Charts, Tickets & receipts
Move from Puppeteer or react-pdfFrom Puppeteer, From react-pdf
Compare takumi-pdf with other toolsComparison

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. 1
landscapebooleanfalseSwaps page width and height, including explicit sizes.
marginnumber, "auto", or { top?, right?, bottom?, left? }"auto"Margin in CSS px. 2
backgroundColorCSS colorunsetFills the whole page, margins included, under everything. 3

1 Size keywords ignore case.

2 A side left out of the object is "auto".

3 Without backgroundColor, viewers show the page as white.

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.

@page rules

A @page rule in css or a <style> element sets the page size and margins. It wins over size, landscape, and margin, as CSS does in Chromium with preferCSSPageSize. :first styles the first page, and :left and :right style pages by the side they fall on. The first page is a right page, or a left page when the root element has direction: rtl.

A cover page, then mirrored margins
@page {
  size: A5;
  margin: 10mm;
}
@page :first {
  margin-top: 30mm;
}
@page :left {
  margin-left: 25mm;
}
@page :right {
  margin-right: 25mm;
}
  • size takes auto, one or two lengths, a keyword from the table above, portrait, or landscape.
  • A margin percentage is a share of the page height for the top and bottom margins, and of the page width for the left and right.
  • margin: auto is 0, as in Chromium. To leave room for a band, leave that side out of the rule so the "auto" option applies.
  • A rule whose margins leave no page area is ignored. A header or footer that leaves no page area fails the render, as it does with the margin option.
  • em, rem, and var() read the root element's font size and custom properties. vw and vh read the page the options give, before size changes it.

Margin boxes such as @top-center and other descriptors are not read.

Named pages

The page property puts an element on a named page, which @page rules with that name style. An element on a different named page than the content before it starts a new page, and its children stay on that page type unless they name their own. Content without a page goes back to the unnamed pages.

A landscape page for a wide table
@page wide {
  size: A4 landscape;
  margin: 10mm;
}
.wide-table {
  page: wide;
}

@page wide:first styles the first page of the document when it is a wide page, as :first does for unnamed pages. A name without a rule of its own takes the unnamed pages' rules.

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({ cache }) sets the byte budget of each cache the renderer keeps, including the fonts that renders' fonts lists load. See Caching.

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), and 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.

Last updated on