Takumi

PDF

Render JSX to paged, selectable-text PDF with takumi-pdf.

takumi-pdf renders the same JSX, Tailwind classes, and node trees as image output, but writes a paged vector PDF: selectable and searchable text, embedded subset fonts, page breaks, and repeating headers and footers. It ships as a WebAssembly module and runs on Node.js, Bun, and Cloudflare Workers without Chromium.

npm i takumi-pdf

Render a document

import { render } from "takumi-pdf";
import { googleFonts } from "@takumi-rs/helpers";
import { writeFile } from "node:fs/promises";

const pdf = await render(<Invoice data={data} />, {
  // A4 portrait with a 48px margin is the default
  size: "a4",
  fonts: await googleFonts(["Inter"]),
  footer: (
    <div tw="flex w-full justify-center text-[10px] text-gray-500">
      Page <span className="pageNumber" /> of <span className="totalPages" />
    </div>
  ),
});

await writeFile("invoice.pdf", pdf);

render() accepts JSX, HTML strings converted with @takumi-rs/helpers, or JSON node trees, and returns Uint8Array PDF bytes. Content lays out at the page's content width and flows onto as many pages as it needs.

Page setup

const pdf = await render(report, {
  size: "letter", // "a4", "letter", or { width, height } in CSS px at 96 dpi
  landscape: true,
  margin: { top: 48, right: 32, bottom: 48, left: 32 },
});
OptionTypeDefaultDescription
size"a4", "letter", or { width, height }"a4"Page size in CSS px at 96 dpi. Presets ignore case.
landscapebooleanfalseSwaps page width and height, including explicit sizes.
marginnumber or { top?, right?, bottom?, left? }48A number applies to all sides. Missing object sides are 0.

@page CSS rules are not supported; these options are the page geometry.

Reuse a renderer

render() keeps one shared renderer alive. When an application manages several font sets, construct PdfRenderer directly:

import { PdfRenderer } from "takumi-pdf";

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

const pdf = await renderer.render(doc);

Registered fonts deduplicate across calls, so rendering many documents pays the font cost once.

Last updated on

On this page