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.
Install
npm i takumi-pdf @takumi-rs/helpersRender a document
Save this as invoice.tsx and run bun invoice.tsx to write invoice.pdf. On Node.js, compile the JSX first.
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:
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
| Task | Guide |
|---|---|
| Control page breaks and repeated table headers | Pagination |
| Add page numbers, headers, and footers | Headers & footers |
| Load fonts, logos, and photos | Fonts & images |
| Add links, a table of contents, or bookmarks | Links, outline & metadata |
| Create a receipt, certificate, or label | Single-page PDFs |
| Generate archival or accessible output | PDF/A & PDF/UA |
| Embed invoice XML or another file | Attachments |
| Build a complete document | Invoices, Reports & statements, Charts, Tickets & receipts |
| Move from Puppeteer or react-pdf | From Puppeteer, From react-pdf |
| Compare takumi-pdf with other tools | Comparison |
Page setup
import { } from "takumi-pdf";
const = await (, {
: "letter", // a page keyword, or { width, height } in CSS px at 96 dpi
: true,
: { : 48, : 32, : 48, : 32 },
});| Option | Type | Default | Description |
|---|---|---|---|
size | a page keyword or { width, height } | "a4" | Page size in CSS px at 96 dpi. Keywords ignore case. |
landscape | boolean | false | Swaps page width and height, including explicit sizes. |
margin | number, "auto", or { top?, right?, bottom?, left? } | "auto" | Margin in CSS px. A side left out of the object is "auto". |
backgroundColor | CSS color | unset | Fills the whole page, margins included, under everything. Viewers show unset as white. |
Each keyword is a portrait paper size:
| Keyword | Size | Keyword | Size |
|---|---|---|---|
"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, bottom | The header or footer height plus a 20px inset, and at least 37.8px (1cm) |
| Left, right | 37.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.
@page {
size: A5;
margin: 10mm;
}
@page :first {
margin-top: 30mm;
}
@page :left {
margin-left: 25mm;
}
@page :right {
margin-right: 25mm;
}sizetakesauto, one or two lengths, a keyword from the table above,portrait, orlandscape.- 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: autois 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
marginoption. em,rem, andvar()read the root element's font size and custom properties.vwandvhread the page the options give, beforesizechanges 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.
@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:
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.
| Method | Effect |
|---|---|
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.
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:
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