jsPDF: PDF Sorcery Right in the Browser
There are moments in web development when you need to hand the user a PDF -- an invoice after checkout, a certificate after a course, a report after crunching numbers. The traditional approach sends the data to a server, generates the file there, and streams it back. jsPDF skips all of that. It builds the PDF entirely in the browser, byte by byte, and hands it to the user without a single network request leaving the page. Originally created by James Hall back in 2009 and now co-maintained by the diagramming experts at yWorks, jspdf has grown into one of the most downloaded PDF libraries in the JavaScript ecosystem -- roughly 8.8 million weekly downloads and over 31,000 GitHub stars.
What Lands on the Page
jsPDF packs a surprisingly deep feature set for a client-side library:
- Text rendering with configurable fonts, sizes, colors, and character spacing
- Vector graphics including circles, ellipses, rectangles, rounded rectangles, triangles, and arbitrary paths with Bezier curves
- Image embedding for JPEG and PNG formats
- Multi-page documents with full page management -- add, delete, insert, reorder
- Custom fonts including TTF loading for full UTF-8 support (Chinese, Arabic, Cyrillic, and beyond)
- Two API modes -- a compatibility mode for straightforward work and an advanced mode for transformation matrices, patterns, and form objects
- AcroForm fields for interactive PDF forms
- TypeScript typings out of the box
- Dual environment support -- runs in the browser and in Node.js
Unrolling the Scroll
Install jspdf with your package manager of choice:
npm install jspdf
or
yarn add jspdf
That is all you need. No native binaries, no system dependencies. The library ships multiple module formats (ESM, UMD, and a Node-specific build), so it works with any bundler or directly via a script tag.
Your First Document
Hello, PDF
The simplest possible document takes three lines:
import { jsPDF } from "jspdf";
const doc = new jsPDF();
doc.text("Hello world!", 10, 10);
doc.save("hello.pdf");
The constructor creates an A4 portrait document by default. The text method places a string at the given x and y coordinates (in millimeters). The save method triggers a browser download. That is a complete, valid PDF.
Tweaking the Canvas
You can customize the page format, orientation, and unit system at construction time:
import { jsPDF } from "jspdf";
const doc = new jsPDF({
orientation: "landscape",
unit: "in",
format: "letter",
});
doc.setFontSize(24);
doc.setTextColor(40, 40, 40);
doc.text("Quarterly Revenue Report", 1, 1);
doc.setFontSize(12);
doc.setTextColor(100, 100, 100);
doc.text("Generated on: 2026-02-19", 1, 1.5);
doc.save("report.pdf");
The unit option accepts "mm", "cm", "in", "px", "pt", or "em". The format option takes standard names like "a4", "letter", "legal", or a custom [width, height] array.
Drawing Shapes and Lines
Every shape method places geometry at exact coordinates. Here is a simple branded header:
import { jsPDF } from "jspdf";
const doc = new jsPDF();
doc.setFillColor(34, 49, 63);
doc.rect(0, 0, 210, 30, "F");
doc.setFontSize(20);
doc.setTextColor(255, 255, 255);
doc.text("ACME INVOICE", 15, 20);
doc.setDrawColor(52, 152, 219);
doc.setLineWidth(0.5);
doc.line(15, 35, 195, 35);
doc.setFontSize(11);
doc.setTextColor(60, 60, 60);
doc.text("Invoice #: 2026-0042", 15, 45);
doc.text("Date: February 19, 2026", 15, 52);
doc.text("Due: March 19, 2026", 15, 59);
doc.save("invoice.pdf");
The rect method with "F" fills the rectangle. Use "S" for stroke only, or "FD" for fill and draw. The same pattern applies to circle, ellipse, and triangle.
Beyond the Basics
Multi-Page Documents with Images
Real-world PDFs rarely fit on one page. Adding pages and embedding images is straightforward:
import { jsPDF } from "jspdf";
async function generateCatalog(
items: Array<{ name: string; description: string; imageDataUrl: string }>
): Promise<void> {
const doc = new jsPDF();
const pageWidth = doc.internal.pageSize.getWidth();
const margin = 15;
items.forEach((item, index) => {
if (index > 0) {
doc.addPage();
}
doc.addImage(item.imageDataUrl, "JPEG", margin, margin, 80, 60);
doc.setFontSize(18);
doc.setTextColor(33, 33, 33);
doc.text(item.name, margin, 85);
doc.setFontSize(11);
doc.setTextColor(80, 80, 80);
const lines = doc.splitTextToSize(
item.description,
pageWidth - margin * 2
);
doc.text(lines, margin, 95);
doc.setFontSize(9);
doc.setTextColor(150, 150, 150);
doc.text(
`Page ${index + 1} of ${items.length}`,
pageWidth - margin - 30,
doc.internal.pageSize.getHeight() - 10
);
});
doc.save("catalog.pdf");
}
The splitTextToSize utility is invaluable -- it wraps long text to fit within a given width, returning an array of lines that text can render directly. The addImage method accepts data URLs, base64 strings, or raw image data.
Custom Fonts for International Text
The 14 built-in PDF fonts only cover ASCII. If your documents need to display Chinese, Arabic, Hebrew, or any other script, you need to load a TTF font:
import { jsPDF } from "jspdf";
async function loadFont(url: string): Promise<string> {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const binary = Array.from(new Uint8Array(buffer))
.map((byte) => String.fromCharCode(byte))
.join("");
return btoa(binary);
}
async function generateLocalizedDoc(): Promise<void> {
const doc = new jsPDF();
const fontBase64 = await loadFont("/fonts/NotoSansCJK-Regular.ttf");
doc.addFileToVFS("NotoSansCJK.ttf", fontBase64);
doc.addFont("NotoSansCJK.ttf", "NotoSansCJK", "normal");
doc.setFont("NotoSansCJK");
doc.setFontSize(16);
doc.text("PDF generation test", 15, 20);
doc.save("localized.pdf");
}
The workflow is: load the font binary, register it with the virtual file system via addFileToVFS, declare it with addFont, then activate it with setFont. After that, all subsequent text calls use the new font.
The Advanced API Mode
jsPDF ships with two API modes. The default compatibility mode covers most use cases, but the advanced mode unlocks transformation matrices, patterns, and form objects:
import { jsPDF } from "jspdf";
const doc = new jsPDF();
doc.advancedAPI((doc) => {
const matrix = doc.Matrix(
Math.cos(Math.PI / 6),
Math.sin(Math.PI / 6),
-Math.sin(Math.PI / 6),
Math.cos(Math.PI / 6),
50,
120
);
doc.setCurrentTransformationMatrix(matrix);
doc.setFontSize(14);
doc.text("Rotated text at 30 degrees", 0, 0);
});
doc.setFontSize(12);
doc.text("This text is back to normal orientation.", 15, 180);
doc.save("advanced.pdf");
The callback-based API is designed so that jsPDF automatically switches back to the original mode once the callback finishes. You can nest advancedAPI and compatAPI calls without worrying about state leaking between them.
Node.js Security Model (v4.0+)
Version 4.0.0 introduced a security-first approach to file system access in Node.js. Previously, jsPDF could read any file on disk. Now file access is restricted by default:
import { jsPDF } from "jspdf";
const doc = new jsPDF();
doc.allowFsRead = ["./fonts/*", "./images/logo.png"];
doc.addImage("./images/logo.png", "PNG", 10, 10, 50, 30);
doc.save("secure.pdf");
The recommended approach is to use Node's built-in permission flags instead:
node --permission --allow-fs-read=./fonts,./images ./generate-report.ts
This keeps your application's security boundary explicit rather than buried in library configuration.
Webpack Configuration for Lighter Bundles
The full jspdf bundle weighs around 290KB. If you are not using optional features like SVG rendering or HTML sanitization, you can shave off unused optional dependencies by declaring them as externals:
// webpack.config.ts
import type { Configuration } from "webpack";
const config: Configuration = {
externals: {
canvg: "canvg",
html2canvas: "html2canvas",
dompurify: "dompurify",
},
};
export default config;
This tells Webpack not to bundle those optional packages, which prevents warnings during builds and keeps your output leaner.
The Final Page
jsPDF has been around for over 15 years, and its longevity is no accident. It fills a specific niche perfectly: programmatic PDF generation that runs anywhere JavaScript runs, with no server required. The library handles everything from single-page receipts to multi-page catalogs with embedded images, custom fonts, and vector graphics.
Version 4.x brought meaningful security improvements, especially the restricted file system access model for Node.js and a batch of vulnerability fixes in 4.1.0 covering PDF injection, metadata injection, and race conditions. The project remains actively maintained with 190+ contributors and a regular release cadence.
Where jsPDF truly shines is in its simplicity. There is no declarative DSL to learn, no component model to adopt. You call methods, things appear on the page, and you save the result. For applications that need to generate invoices at checkout, export dashboards as PDFs, or produce certificates on the fly, that directness is exactly what you want. When your requirements grow beyond what the core library offers, the plugin ecosystem -- most notably jsPDF-AutoTable for table generation -- extends its reach further.
If your next feature involves the words "download as PDF," jspdf should be at the top of your shortlist.