Generate PDFs from HTML in React with jsPDF

This post will walk you through the process of using jsPDF, a popular client-side library. Whether you’re creating a report or a ticket, this tutorial will help you smoothly integrate PDF generation into your React project.
Generate PDFs from HTML in React with jsPDF
TL;DR

Build printable reports, invoices, or tickets in React without a server. This guide shows how to import jsPDF(opens in a new tab), reference your JSX with useRef, and export a polished PDF — plus tricks for custom page size, multiple pages, and custom fonts. jsPDF works for basic needs but lacks external CSS support and requires a font converter for non‑standard fonts.

What can you convert into PDF with jsPDF?

If you look at the live demo examples(opens in a new tab) of jsPDF, you’ll see it’s possible to convert images, font faces, font sizes, circles, rectangles, triangles, tables, lines, languages, and more into PDF format. You can also convert HTML into multiple pages with page breaks and add password protection and annotations. It’s also possible to change the orientation of a page to landscape or portrait.

Setting up jsPDF in your React project

To get started with React HTML-to-PDF conversion, you’ll use Vite(opens in a new tab). This will create a new React project with all the necessary dependencies and configuration files:

Terminal window
npm create vite@latest convert-html-to-pdf -- --template react

Change to the newly created directory and install the jspdf package through npm or yarn:

Terminal window
cd convert-html-to-pdf
Terminal window
npm install
Terminal window
npm i jspdf

Step-by-step guide to converting HTML to PDF

  1. Now, open the App.jsx file and replace the default code with the following:
import jsPDF from 'jspdf';
// Initialize a new instance of `jsPDF`:
const doc = new jsPDF();

jsPDF provides some options to customize a PDF. By default, it’ll use A4 paper size, portrait orientation, and millimeters as a unit of measurement.

If you want to change any of these options, you can pass an object to the constructor:

const doc = new jsPDF({
orientation: 'landscape', // or "portrait"
unit: 'px', // "mm", "cm", "in" also supported
format: [4, 2], // array like [width, height]
});

You’ll find all the available options in the jsPDF documentation(opens in a new tab).

  1. jsPDF provides a method called html()(opens in a new tab) to convert HTML to PDF. It takes two arguments: the HTML element, and a callback function. Since you’re using React, to get a reference to the HTML element, use the useRef()(opens in a new tab) hook.

The doc.save() method takes the name of the PDF file as an argument. It’ll download the PDF file to the user’s computer:

doc.html(html_element, {
async callback(doc) {
await doc.save('pdf_name');
},
});

Adding the markup

To convert HTML to PDF, you need to add the markup to the page. You’ll use a report template with a button. When the button is clicked, you’ll trigger an event to generate the PDF.

Create a new file called ReportTemplate.jsx and add the following code:

const styles = { img: { width: '100%' }, h1: { marginBottom: '1rem' } };
import SalesChart from './sales-chart.png';
export default function ReportTemplate() {
return (
<div
style={{
padding: '40px',
background: 'white',
color: 'black',
}}
>
<h1 style={styles.h1}>Sales Report 2025</h1>
<img src={SalesChart} style={styles.img} alt="Sales chart" />
</div>
);
}

Add a sample image to the src folder and name it sales-chart.png. You can use any image you want. The image will be included in the PDF.

Generating the PDF

Now that you have the JSX(opens in a new tab) in place, you can handle the button click. You’ll use the onClick event handler to call the handlePDF function.

Go back to App.jsx and add the onClick event handler to the button. After that, import the useRef hook and the reference to the HTML element:

import { useRef } from 'react';
import jsPDF from 'jspdf';
import ReportTemplate from './ReportTemplate';
function App() {
const reportTemplateRef = useRef(null);
const handleGeneratePdf = () => {
const doc = new jsPDF({
format: 'a4',
unit: 'px',
});
// Adding the fonts.
doc.setFont('Inter-Regular', 'normal');
doc.html(reportTemplateRef.current, {
async callback(doc) {
await doc.save('document');
},
});
};
return (
<div>
<button className="button" onClick={handleGeneratePdf}>
Generate PDF
</button>
<div ref={reportTemplateRef}>
<ReportTemplate />
</div>
</div>
);
}
export default App;

The app is now ready to generate PDFs. You can run the app with npm run dev and click the button to generate the PDF.

Adding custom fonts

jsPDF has support for 14 standard PDF fonts. To add custom fonts, you need to use a font converter. Navigate to this font converter(opens in a new tab) and upload .ttf files.

Select Choose Files to choose the fonts you want to add. After that, click Create to convert the fonts. You don’t need to modify the fontName, fontStyle, or Module format fields.

font converter

The font converter will generate a JavaScript file with the provided .ttf file as a Base64-encoded string and some code for jsPDF. Add the generated JavaScript file to your project.

To activate the custom font, use the setFont()(opens in a new tab) method:

doc.setFont('Inter-Regular', 'normal');

You can find the font name before you convert the font to JavaScript. If you’re using more than one font, you need to go through the same process for each font.

Known limitations of jsPDF

LimitationWorkaround
External CSS is not automatically loadedApply styles via inline CSS or imported stylesheets at build-time, or ensure styles are rendered in the DOM before calling html().
Custom fonts require manual conversionUse jsPDF’s font converter tool to generate font files compatible with jsPDF and embed them manually.
Large or complex HTML content may overflow or get cut offManually implement pagination logic, or split content into smaller sections before rendering to PDF.
Sparse official documentationRely on community examples and GitHub issues, or inspect jsPDF source code for advanced use cases.

For enterprise-grade features like text search, annotations, redaction, digital signatures, and pixel-perfect rendering, consider using a dedicated solution such as the Nutrient React PDF SDK.

Advanced techniques with jsPDF and helpers

Below are some optional addons and best practices if you need more than basic HTML‑to‑PDF export.

Capture styled components with html2canvas

Because jsPDF ignores external stylesheets, a popular workaround is to pair it with html2canvas(opens in a new tab). The library rasterizes an element (honoring CSS) to a canvas, which you can then drop into the PDF.

When your component relies on external CSS (flex/grid, classes, web fonts), jsPDF’s html() rendering falls short. A common workaround is to rasterize the node with html2canvas(opens in a new tab), and then embed the screenshot into a PDF. We keep this logic in a reusable helper so every page can call it.

Create a helper file (src/utils/pdfHelpers.js):

import html2canvas from 'html2canvas';
import { jsPDF } from 'jspdf';
/**
* Generates an A4 PDF snapshot of a DOM node with full CSS support.
* @param {HTMLElement} node – the element to capture
* @param {string} filename – optional, defaults to "styled.pdf"
*/
export async function downloadStyledPdf(node, filename = 'styled.pdf') {
const canvas = await html2canvas(node, { useCORS: true, scale: 2 }); // higher scale = crisper output
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({ unit: 'px', format: 'a4' });
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = (canvas.height * pageWidth) / canvas.width;
pdf.addImage(imgData, 'PNG', 0, 0, pageWidth, pageHeight);
pdf.save(filename); // → defaults to styled.pdf
}

This code grabs a snapshot of your styled DOM node with html2canvas, turns that canvas into a PNG, and drops it onto an A4 page in jsPDF. Finally, it triggers a download — which is named styled.pdf by default — so users get a pixel‑perfect copy.

html2canvas is pixel‑based, so vector text becomes an image. For crisp text, use server‑side rendering or a true PDF layout engine.

Use the helper in your component:

import { useRef } from 'react';
import { downloadStyledPdf } from './utils/pdfHelpers';
import ReportTemplate from './ReportTemplate';
export default function App() {
const reportRef = useRef(null);
const handleStyledExport = async () => {
if (reportRef.current) {
await downloadStyledPdf(reportRef.current); // saves styled.pdf
}
};
return (
<>
<button onClick={handleStyledExport}>Export Styled PDF</button>
<div ref={reportRef}>
<ReportTemplate />
</div>
</>
);
}
  • Drawbacks — The PDF is an image, so text can’t be searched or selected. For vector text, fall back to doc.html().

Generate paginated tables with jspdf-autotable

If you export spreadsheets or invoices, install the jspdf-autoTable(opens in a new tab) plugin:

Terminal window
npm i jspdf-autotable
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
export default function App() {
// 1. Build your rows (array-of-arrays)
const dataArray = [
['Ada Lovelace', '[email protected]', 'UK'],
['Grace Hopper', '[email protected]', 'USA'],
['Alan Turing', '[email protected]', 'UK'],
];
const pdf = new jsPDF();
autoTable(pdf, {
head: [['Name', 'Email', 'Country']],
body: dataArray,
styles: { fontSize: 10 },
});
pdf.save('table.pdf');
}

Here, head defines the header row, body supplies one nested array per data line, and autoTable automatically sizes columns, paginates overflow rows, and repeats the header on every new page. The optional styles object applies global cell formatting. This sets every cell’s font size to 10px.

Encrypt and password‑protect your PDF (built‑in)

Since jsPDF v2.5, encryption is native(opens in a new tab) and no extra plugin is required. Supply a userPassword, an ownerPassword, and a permissions array when you instantiate the document.

Basic encryption

import { jsPDF } from 'jspdf';
const doc = new jsPDF({
encryption: {
userPassword: 'user123', // required to open the file
ownerPassword: 'admin456', // controls editing rights
permissions: [
'print',
'modify',
'copy',
'annot-forms', // allowed actions
],
},
});
doc.text('Top‑secret numbers', 20, 20);
doc.save('protected.pdf');
  • Algorithm — 128‑bit AES is the default (stronger than the old RC4‑40).
  • Permissions — Omit an item to disallow it. Available flags: print, print-low-resolution, modify, copy, annot-forms, fill-forms.

Granular permissions

Limit users to low‑resolution printing only:

const doc = new jsPDF({
encryption: {
userPassword: 'viewer',
ownerPassword: 'masterkey',
permissions: ['print-low-resolution'],
encryptionType: 128, // AES‑128 (default)
},
});

Change encryption strength

const legacyDoc = new jsPDF({
encryption: {
userPassword: 'secret',
encryptionType: 40, // RC4‑40 — smaller file, legacy compatibility
},
});

Best practice — Stick with AES‑128 for modern apps. Use RC4‑40 only when you must support very old PDF readers.

Add metadata to your PDF

import { useRef } from 'react';
import jsPDF from 'jspdf';
import ReportTemplate from './ReportTemplate';
export default function App() {
const reportRef = useRef(null);
const handleExport = async () => {
if (reportRef.current) {
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'px',
format: 'a4',
});
await pdf.html(reportRef.current, {
async callback(doc) {
// Set PDF metadata properties.
doc.setProperties({
title: 'Sales report 2025',
subject: 'Quarterly numbers',
author: 'ACME Corp',
keywords: 'react, pdf, sales',
creator: 'React app',
});
// Save the PDF file.
doc.save('report.pdf');
},
});
}
};
return (
<>
<button onClick={handleExport}>Export PDF</button>
<div ref={reportRef}>
<ReportTemplate />
</div>
</>
);
}

doc.setProperties() embeds descriptive metadata — title, subject, author, keywords, and the generating app — directly into the PDF file so viewers like Acrobat display that information and search engines can index it. Immediately after, doc.save("report.pdf") streams the finished document to the browser and prompts the user to download it under the specified filename.

When to choose another tool

Use casejsPDF@react-pdf/rendererPuppeteer / Playwrightpdf-libNutrient React Viewer
Client-only export❌ (server)
External CSS—*
Vector text
Complex layout (flex/grid)🚫🚫
Password protection🚫
File sizeSmallMediumLargeSmallMedium
Learning curveLowMediumMediumMediumMedium

* The Nutrient React viewer renders existing PDF content, so external CSS limitations don’t apply.

Conclusion

This guide showed you how to turn HTML or JSX into PDFs directly in the browser with jsPDF. When you need heavier-duty features — viewing, searching, commenting, redacting, or signing — the commercial Nutrient React viewer integrates seamlessly with any React project and features 30+ tools, along with a slick, customizable UI.

If server-side automation is on your roadmap, the same core engine powers our DWS Processor API product, and the open source MCP server lets AI assistants like Claude reach those endpoints with everyday language.

Start a free trial or launch the live demo to see the viewer in action.

FAQ

What advantages does the Nutrient React viewer offer over jsPDF?

jsPDF excels at quick, lightweight HTML-to-PDF export, but that’s largely where it stops. The Nutrient React viewer goes much further: It renders PDFs with pixel-perfect fidelity, and it supports full-text search, annotation, redaction, digital signatures, form filling, and secure permission controls — all in one package. If your project demands professional-grade document handling rather than simple static exports, Nutrient provides the broader, more powerful toolset.

Can jsPDF use my existing CSS files?

Not directly. html() only sees styles already in the DOM. Inline your critical rules, or rasterize with html2canvas. If you need pixel-perfect CSS, a headless browser or the Nutrient React viewer is a better fit.

How do I add password protection with jsPDF?

Since v2.5, you can pass a userPassword, an ownerPassword, and a permissions array when creating the document. Refer to the section on encrypting and password protection above.

Does the Nutrient React viewer work offline?

Yes. The viewer itself is a client-side bundle. Cloud features (like the Processor API) are optional and can be self-hosted for fully on-premises deployments.

Hulya Masharipov

Hulya Masharipov

Technical Writer

Hulya is a frontend web developer and technical writer at Nutrient who enjoys creating responsive, scalable, and maintainable web experiences. She’s passionate about open source, web accessibility, cybersecurity privacy, and blockchain.

Explore related topics

FREE TRIAL Ready to get started?