boxpdf

Open-source HTML-to-PDF for JavaScript runtimes

Render HTML or code
into PDFs that run anywhere.

boxpdf is a tiny OSS toolkit for server-side PDF generation: render document-style HTML + Tailwind CSS with boxpdf-html, or build layouts directly with a flexbox-lite DSL over pdf-lib. Runs in Node, Cloudflare Workers, Deno, and the browser. No WASM. No headless browser. No React.

HTML + Tailwind input MIT open source Edge-ready PDFs Bounded-memory streaming PDF 2.0 AES-256
import { cleanTheme, hline, hstack, renderFlow, text, vstack } from "boxpdf";
import { PDFDocument, StandardFonts } from "pdf-lib";

const pdf = await PDFDocument.create();
const font = await pdf.embedFont(StandardFonts.Helvetica);
const bold = await pdf.embedFont(StandardFonts.HelveticaBold);
const theme = cleanTheme(font, bold);

await renderFlow(pdf, [
  vstack({ gap: 8 },
    text("Receipt #18472", theme.type.h1),
    text("May 14, 2026",     theme.type.caption)
  ),
  hline(theme.hr),
  hstack({ gap: 16 },
    text("Wool socks",   theme.type.body),
    text("$28.00",        { ...theme.type.body, font: bold, align: "right", width: 80 })
  )
]);

const bytes = await pdf.save(); // -> Uint8Array

What's in the box

Runs at the edge

Works on Cloudflare Workers without nodejs_compat or any WASM flag. Verified end-to-end with embedded Inter.

Flex-ish layout, no coordinates

vstack / hstack with padding, margin, gap, justify, align, grow, shrink. Real word-wrapping and ellipsis.

Streaming output

streamFlow emits bytes to a WritableStream as each page closes. In the 1000-page benchmark it used 25.4 MB peak heap versus 219.6 MB for renderFlow + pdf.save().

PDF 2.0 AES-256 encryption

Password encryption happens during serialization—including streamed output—with optional owner credentials and viewer permissions. No temporary plaintext PDF or encryption post-process.

Four named themes

Drop-in cleanTheme, stripeTheme, editorialTheme, brutalistTheme. Same code restyles every template.

Multi-page flow

renderFlow paginates with atomic children. keepTogether bundles widows. Page headers and footers with { pageNumber, totalPages } built in.

Bring your own fonts

Optional boxpdf/inter ships subsetted Inter (82 KB / weight). loadFont(pdf, source) and the boxpdf font add CLI bundle any TTF as base64.

Hyperlinks, decorations, metadata

link({ href }, ...), underline, strikethrough, title / author / subject as renderFlow options.

Debug overlay

renderFlow(pdf, nodes, { debug: true }) outlines every content and margin box in red and orange. Trace layouts visually.

Tiny, tree-shakable

Core is <7 KB minified. boxpdf/inter and @pdf-lib/fontkit only load when you use them.

HTML and Tailwind to PDF

boxpdf-html is the open-source HTML renderer for boxpdf. It turns document-style HTML and generated Tailwind CSS into boxpdf primitives, so you can ship PDF output without a browser runtime.

<div class="p-6 bg-[#f8fafc] text-gray-900">
  <div class="max-w-[520px] rounded-[10px] border bg-white p-5 shadow-sm">
    <div class="grid grid-cols-[1fr_2fr] gap-x-4 gap-y-3">
      <div class="rounded-md border border-blue-200 bg-blue-50 p-3">
        <p class="text-xs font-semibold uppercase tracking-wide text-blue-700">Status</p>
        <p class="mt-1 text-sm font-bold">Paid</p>
      </div>
      <div class="rounded-md border border-gray-200 p-3">
        <p class="text-xs font-semibold uppercase tracking-wide text-gray-600">Notes</p>
        <p class="mt-1 text-sm leading-5">Two fraction column wraps later.</p>
      </div>
    </div>

    <div class="mt-4 flex items-center justify-between rounded-md bg-gray-900 px-4 py-3 text-white">
      <span class="text-sm font-semibold">Amount due</span>
      <span class="text-xl font-bold leading-none">$1,250.00</span>
    </div>
  </div>
</div>
Tailwind utility HTML rendered by boxpdf-html
Tailwind utilities generated utilities · grid · flex · arbitrary values html/fixtures

Bounded-memory HTML streaming

Large HTML, without the large heap

The HTML CLI makes two bounded passes: one to discover CSS, fonts, and images, then one to parse, lay out, and write the PDF incrementally. A single open wrapper can span every fragment, so large real-world documents do not need artificial page-sized roots.

Stream from a file or stdin

npx boxpdf-html archive.html archive.pdf --stream

cat archive.html | npx boxpdf-html - archive.pdf --stream

The destination is replaced only after conversion succeeds. Stdin is spooled to a temporary file for the required second pass.

Measured with one continuous wrapper

10×HTML input growth
10 → 100 MiB
1.54×peak RSS growth
141.7 → 217.8 MiB
+0.8 MiBpeak JS heap growth

The visual test suite raster-compares every streamed page against buffered output. Inspect the benchmark and visual-parity evidence.

Four named themes

Same layout code, four aesthetics. Swap the theme and every template restyles.

Clean theme. Modern SaaS, soft borders, 8pt rounded corners
cleanTheme modern SaaS default
Stripe theme. Square corners, thin borders, monochrome with accent
stripeTheme square corners, thin borders, monochrome
Editorial theme. Times serif, warm cream, italic captions
editorialTheme Times serif, generous leading
Brutalist theme. Courier monospace, 2pt solid black borders
brutalistTheme Courier monospace, 2pt black borders

Template gallery

Production-ready documents in templates/. Copy a file, edit it for your data, ship it.

Receipt template
Receipt cleanTheme · 1 page templates/receipt.ts
Boarding pass template
Boarding pass cleanTheme · 1 page templates/boarding-pass.ts
Resume template
Resume editorialTheme · 2 pages templates/resume.ts
Order confirmation template
Order confirmation cleanTheme · 1 page templates/order-confirmation.ts
Certificate template
Certificate editorialTheme · landscape templates/certificate.ts
Multi-page invoice with page headers and footers
Invoice cleanTheme · 2 pages, header/footer, keepTogether examples/invoice.ts
Travel itinerary with two flight bands
Itinerary cleanTheme · two-band layout examples/itinerary.ts
Debug overlay with red content boxes and orange margin boxes
Debug overlay demo cleanTheme · { debug: true } examples/debug.ts
Absolute positioning debug example with top-right stamp and nested overlay
Absolute positioning debug overlay · position relative/absolute examples/absolute-positioning.ts
Rich paragraph runs and styled table cells debug example
Rich content paragraph runs · styled table cells examples/rich-content.ts
Hanging indent paragraph example
Hanging indent paragraph paddingLeft · negative textIndent examples/hanging-indent.ts
Overflow clipping with absolute overlay and background image
Overflow clipping overflow hidden · backgroundImage · imageFit examples/overflow-clipping.ts

Install

npm install boxpdf pdf-lib

pdf-lib is a peer dependency. If you want custom-font embedding (incl. boxpdf/inter), @pdf-lib/fontkit comes along for the ride and is lazy-loaded only when you embed a non-standard font.

Cloudflare Workers

Both the core and the boxpdf/inter subpath are verified to run on Cloudflare Workers without nodejs_compat or any WASM flag. Drop it into a worker handler:

import { Hono } from "hono";
import { PDFDocument, StandardFonts } from "pdf-lib";
import { cleanTheme, renderFlow, text, vstack } from "boxpdf";

const app = new Hono();

app.get("/receipt.pdf", async (c) => {
  const pdf  = await PDFDocument.create();
  const font = await pdf.embedFont(StandardFonts.Helvetica);
  const bold = await pdf.embedFont(StandardFonts.HelveticaBold);
  const t    = cleanTheme(font, bold);
  await renderFlow(pdf, [
    text("Thanks!", t.type.h1),
    text("This PDF was generated at the edge.", t.type.body)
  ]);
  const bytes = await pdf.save();
  return new Response(bytes, { headers: { "content-type": "application/pdf" } });
});

export default app;

How it compares

boxpdfpdf-lib@react-pdf/rendererjsPDF
Declarative layout✓ (JSX)
Cloudflare Workers✗ (fontkit WASM)partial
Streaming outputstreamFlow (Web Writable, bounded memory)Node only
Custom fonts✓ via fontkit (lazy)✓ via fontkitlimited
Core bundle~7 KB gz~250 KB gz~250 KB gz~80 KB gz
JSX runtime conflictnonen/arequires Reactn/a

Sizes are approximate. Measure yourself.

Memory bench

Peak heap during render. 50 lines of text per page. Each measurement runs in its own subprocess. @react-pdf/renderer uses Standard Helvetica (no font embedding) to match the boxpdf paths.

Peak heap during render across page counts for streamFlow, renderFlow, and @react-pdf/renderer

PagesstreamFlowrenderFlow@react-pdf/rendererOutput
5012.8 MB31.7 MB160.8 MB70 KB
25015.4 MB91.1 MB643.1 MB347 KB
50018.7 MB120.8 MB1,219.9 MB693 KB
100025.4 MB219.6 MB2,292.6 MB1.4 MB

Bench source: scripts/bench-memory.ts. Design doc: docs/design/streaming.md.

Encryption built into the writer

Protect buffered and streamed PDFs

boxpdf writes PDF 2.0 Standard Security Handler documents with AES-256. Strings, content streams, fonts, and images are encrypted as objects are serialized, preserving bounded document memory.

Library API

await streamFlow(pdf, writable, rows(), {
  encryption: {
    password: process.env.PDF_PASSWORD,
    permissions: { copying: false }
  }
});

The same encryption option works with savePdf, flowToPdf, and renderToPdf.

CLI, without secrets in process arguments

export BOXPDF_PASSWORD="open me"
npx boxpdf-html archive.html archive.pdf --stream --password-env BOXPDF_PASSWORD

The CLI reads the password from the named environment variable. Owner passwords and viewer permissions are available through the library API. Viewer permissions are advisory, as defined by the PDF standard.

Security design and scope: docs/design/encryption.md.

Supported surface