Escape

JavaScript PDF generator

Writing the PDF bytes is the easy part, and every library here does it. The job is everything before that. Something has to decide where page two starts, repeat the table header across the break, and keep a total with the rows it totals. Something has to embed the fonts, so the file reads the same on a machine that has never heard of them. That’s typesetting, and it’s what you’re really choosing between.

The libraries below split into four approaches. Pick the approach first, and the library usually follows.

The libraries at a glance

Library How you describe the document Runs in License
PDFKit Drawing calls with flowing text Node and the browser MIT
jsPDF Drawing calls, plus plugins The browser and Node MIT
pdf-lib Drawing calls on new or existing files Node and the browser MIT
pdfmake A document object of text, tables, and columns Node and the browser MIT
React-PDF React components Node and the browser MIT
Puppeteer HTML and CSS, printed by Chrome Node, with a Chromium download Apache-2.0
quario A JSON report definition, bound to your data Node, the browser, and edge runtimes Commercial

Drawing calls with PDFKit, jsPDF, and pdf-lib

You place text, lines, and images on the page yourself. They’re small and fast, and they give you complete control over every point on the page.

The cost is that the report’s logic lives in your code. Grouping rows, carrying a subtotal, and keeping a total off an otherwise empty page are all yours to write. That’s fine for a certificate or a shipping label, and tedious for a forty-row invoice.

A document object with pdfmake

You build one JavaScript object describing paragraphs, tables, and columns, and pdfmake lays it out. It paginates for you and repeats table header rows on every page. It’s open source and has been around for years, which makes it the default answer to “a table that spans pages”.

The object is still something your code assembles. Data binding, totals, and number formats happen in JavaScript before pdfmake sees them, so the layout and the logic stay mixed together. One invoice in pdfmake and in quario, side by side →

Components with React-PDF

@react-pdf/renderer lets you write the document as React components laid out with rules borrowed from CSS. It wraps content across pages and can repeat an element on every page. If your team already thinks in components, it’s the most comfortable of the libraries here.

It’s React, though. A service that isn’t otherwise using React takes on a renderer and a JSX build step to produce one document. One quote in React-PDF and in quario, side by side →

HTML and CSS with Puppeteer

Render the report as an HTML page, then print it with headless Chrome’s page.pdf(). You get all of CSS, so this is the easiest way to match a design a designer drew.

You also get Chromium in your container. That’s a download of a few hundred megabytes, a slow cold start, and a browser process to keep alive and patched. Print CSS can control page breaks, but getting a long table to break cleanly takes tuning. Generating a PDF with Puppeteer, and what it costs →

Gotenberg runs the same Chromium, and LibreOffice beside it, as a Docker service behind an HTTP API. That takes the browser out of your app and puts it in a container you run. Gotenberg against a library in-process →

A definition with quario

quario takes a different position on where the report lives. It’s a definition, JSON you store, version and review, bound to the data you hand it at render time. The definition says which rows repeat, what totals to compute, and how to format each value. The PDF target does the typesetting, with no browser involved.

app.ts
import { writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { pdf } from '@quario/pdf'
import definition from './invoice.report.json' with { type: 'json' }

const report = quario({ locale: 'en-IE', currency: 'EUR' }).report(definition)

const data = await getInvoice()                    // your JSON, from an API or DB
const bytes = await report.render(pdf({ page: { size: 'A4', margin: 54 } }), data)

writeFileSync('invoice.pdf', bytes)

@quario/pdf paginates, repeats table headers, keeps blocks together, and embeds the fonts. The bytes are deterministic, so the same definition and data always produce the same file. JSON to PDF walks through the definition itself.

Two things set it apart from the libraries above. The same definition also renders to HTML, an Excel workbook and a Word document, so the on-screen report and the export never drift. And it renders without eval or code generation, so a definition a user or an AI agent wrote can’t run anything. More on that →

Which one to pick

  • A one-page layout you control to the point, like a certificate or a label: PDFKit or jsPDF.
  • Changing a PDF you already have, like filling a form or stamping a page: pdf-lib.
  • A long table in an open-source project: pdfmake.
  • A team that already writes React: React-PDF.
  • A designer’s layout that needs every CSS feature, and room for Chromium: Puppeteer.
  • Data-driven business documents, like invoices, statements, and payslips, that also need a workbook or a Word file: quario.

FAQ

Can I generate a PDF in the browser?

Yes, with every library here except Puppeteer. jsPDF, pdfmake, pdf-lib and React-PDF all run client-side, and so does @quario/pdf. The same quario definition renders on the server and in the browser.

How big is quario?

The engine is under 5 kB minified and compressed. Each render target adds a little on top. HTML adds around 1 kB, and PDF adds around 5 kB. The PDF target also leans on a file writer.

Can I use quario for free?

Evaluating it’s free, with no time limit. You get the full engine and need no key. Evaluation output carries a watermark. Shipping it in an application or running it in your business needs a license for each production deployment. Its users, agents, and renders are never counted, and replicas of one service count once. See pricing →