How to convert HTML to PDF in React with jsPDF and html2pdf.js
Table of contents
Nutrient DWS handles rendering, fonts, and CSS for you — start with 50 free credits.
Use useRef to pass a rendered React element to jsPDF’s html() method, and save the PDF from a button handler. Set an explicit content width and pagination mode to keep long reports within the page. For image-based exports, use html2canvas-pro or html2pdf.js; for server-side conversion, use Nutrient HTML-to-PDF API.
What can jsPDF convert to PDF?
jsPDF(opens in a new tab) generates PDFs in the browser. Its html() method uses html2canvas to interpret the rendered DOM and translate supported text, images, and styles into PDF drawing commands. It doesn’t reproduce every browser layout or CSS feature. Test your actual report template before choosing this approach.
This tutorial builds a React download button, adds multipage output and custom fonts, and covers table and screenshot alternatives. See the jsPDF examples(opens in a new tab) for its other drawing APIs.
Setting up jsPDF for HTML to PDF in React
Use Node.js 22.12 or later. You’ll also need basic familiarity with React. Vite(opens in a new tab) also supports Node.js 20.19+, but Node.js 18 can’t run the current scaffold.
Create the app and install jsPDF with its HTML-rendering dependency:
npm create vite@latest convert-html-to-pdf -- --template reactcd convert-html-to-pdfnpm installnpm install jspdf html2canvasRemove the default styles from src/index.css and the App.css import from App.jsx so Vite’s starter theme doesn’t change the report layout.
Step-by-step: Convert HTML to PDF in React
The example below uses A4 portrait pages and millimeters. jsPDF also accepts named formats such as letter and an orientation option. See the constructor API reference(opens in a new tab) for supported values.
The html()(opens in a new tab) method returns a promise. Wait for it to finish before calling save(), and use useRef()(opens in a new tab) to access the mounted report element.
Adding the markup
Create src/ReportTemplate.jsx. The fixed width gives the exporter a consistent layout regardless of the browser window size:
import SalesChart from './sales-chart.png';
export default function ReportTemplate() { return ( <article style={{ width: 760, boxSizing: 'border-box', padding: 24, background: '#ffffff', color: '#000000', fontFamily: 'Helvetica, Arial, sans-serif', fontSize: 16, lineHeight: 1.5, }}> <h1>Sales report</h1> <p>Quarterly sales by region.</p> <img src={SalesChart} style={{ width: '100%', display: 'block' }} alt="Sales by region" /> <p>End of report.</p> </article> );}Add your chart image to src/sales-chart.png. Use a local image for this example; remote images need compatible cross-origin headers.
Generating the PDF
Replace src/App.jsx with this component. It waits for fonts and images, disables the button during export, and reports failures:
import { useRef, useState } from 'react';import { jsPDF } from 'jspdf';import ReportTemplate from './ReportTemplate';
export default function App() { const reportRef = useRef(null); const [busy, setBusy] = useState(false); const [error, setError] = useState('');
const handleGeneratePdf = async () => { const report = reportRef.current; if (!report || busy) return; setBusy(true); setError(''); try { await document.fonts.ready; await Promise.all([...report.querySelectorAll('img')].map((image) => image.decode())); const pdf = new jsPDF({ format: 'a4', unit: 'mm' }); await pdf.html(report, { margin: [10, 10, 10, 10], x: 0, y: 0, width: 190, windowWidth: 760, autoPaging: 'text', }); pdf.save('report.pdf'); } catch (error) { setError(error instanceof Error ? error.message : 'PDF export failed.'); } finally { setBusy(false); } };
return ( <> <button onClick={handleGeneratePdf} disabled={busy}> {busy ? 'Generating PDF…' : 'Generate PDF'} </button> {error && <p role="alert">{error}</p>} <div style={{ overflowX: 'auto' }}> <div ref={reportRef} style={{ width: 760 }}><ReportTemplate /></div> </div> </> );}Run npm run dev and select Generate PDF. The 760-pixel report fits within 190 millimeters of printable width. autoPaging: 'text' moves overflowing text onto additional pages; it doesn’t guarantee that images or complex table rows stay together.
To check pagination, repeat the report paragraphs until the content spans several pages. Confirm that the final “End of report.” line appears and that page edges don’t cut through important content. Check the downloaded PDF, not just the browser preview.
Adding custom fonts
jsPDF’s standard PDF fonts don’t cover every character. To embed your own static .ttf font, open the font converter(opens in a new tab) and upload a font you’re licensed to embed.
Set fontName to ReportFont, fontStyle to normal, and Module format to ES modules, and select Create. Save the generated ReportFont-normal.js file in src/fonts/.

Import that module before creating a jsPDF document. It registers the font through jsPDF’s addFonts event. This complete component demonstrates embedding it with setFont()(opens in a new tab):
import { jsPDF } from 'jspdf';import './fonts/ReportFont-normal.js';
export default function FontExport() { const exportPdf = () => { const pdf = new jsPDF(); pdf.setFont('ReportFont', 'normal'); pdf.text('Report with an embedded font', 20, 20); pdf.save('custom-font.pdf'); }; return <button onClick={exportPdf}>Export custom font PDF</button>;}Use the exact registered name and style. Generate and import a separate module for each weight or style you need.
For html() exports, setFont() alone doesn’t override the DOM’s font. Load the font in the browser with @font-face, apply its family to the report’s CSS, and use the fontFaces option documented in the html() API reference(opens in a new tab) to map the matching TTF file into the PDF.
Known limitations of jsPDF
| Limitation | What to do |
|---|---|
| Only supported CSS properties are reproduced. | Test the rendered DOM and check the html2canvas feature list(opens in a new tab). Stylesheet origin alone doesn’t determine support. |
| Custom fonts need registration in the PDF. | Embed the required fonts and match the names used in your report. |
| Automatic pagination can split images or complex layouts. | Check every page, or use AutoTable for long data tables. |
| Remote images may be omitted or block export. | Serve images locally or configure cross-origin access; useCORS can’t override the remote server. |
For conversion on your backend, try Nutrient HTML-to-PDF API. Send HTML and its assets from your server, and return the PDF to React. Keep the API key on the server.
Advanced HTML-to-PDF techniques in React
Capture styled components with html2canvas-pro
A screenshot export stores the rendered element as an image. The helper below uses html2canvas-pro(opens in a new tab), which supports additional CSS features such as oklch() colors. It still has rendering limitations; it doesn’t provide full browser CSS support.
Install the same package used in the import:
npm install html2canvas-proCreate src/utils/pdfHelpers.js:
import html2canvas from 'html2canvas-pro';import { jsPDF } from 'jspdf';
export async function downloadStyledPdf(node, filename = 'styled.pdf') { if (!node) throw new Error('The report element is missing.'); await document.fonts.ready; await Promise.all([...node.querySelectorAll('img')].map((image) => image.decode())); const canvas = await html2canvas(node, { useCORS: true, scale: 2, backgroundColor: '#ffffff' }); if (!canvas.width || !canvas.height) throw new Error('The report is empty.'); const pdf = new jsPDF({ unit: 'mm', format: 'a4' }); const margin = 10; const width = pdf.internal.pageSize.getWidth() - margin * 2; const height = pdf.internal.pageSize.getHeight() - margin * 2; const pixelsPerPage = Math.floor(height * canvas.width / width);
for (let top = 0; top < canvas.height; top += pixelsPerPage) { const slice = document.createElement('canvas'); slice.width = canvas.width; slice.height = Math.min(pixelsPerPage, canvas.height - top); const context = slice.getContext('2d'); if (!context) throw new Error('Canvas rendering is unavailable.'); context.drawImage(canvas, 0, top, slice.width, slice.height, 0, 0, slice.width, slice.height); if (top > 0) pdf.addPage(); pdf.addImage(slice.toDataURL('image/png'), 'PNG', margin, margin, width, slice.height * width / slice.width); } pdf.save(filename);}To use the helper, import downloadStyledPdf into App.jsx. Then, in handleGeneratePdf, replace everything inside the existing try block with this call, and remove the now-unused jsPDF import:
await downloadStyledPdf(report);The helper already waits for fonts and images, builds the PDF, and saves it. Keep the existing button state and error handler. The helper slices tall captures across A4 pages, but a slice can cut through a line or image. For row-aware table pagination, use AutoTable below.
Screenshot PDFs contain image-based text that can’t be selected or searched as text. Large captures can also exceed the browser’s canvas limits. Use a text-based renderer when searchable text is required.
Generate paginated tables with jspdf-autotable
Install jspdf-autotable(opens in a new tab) for tables with repeated headers and automatic page breaks:
npm install jspdf-autotableReplace App.jsx with this component to test it. Export runs only when the button is selected:
import { jsPDF } from 'jspdf';import { autoTable } from 'jspdf-autotable';
export default function App() { const exportTable = () => { const pdf = new jsPDF(); autoTable(pdf, { head: [['Name', 'Email', 'Country']], body: [ ['Ada Lovelace', 'ada@example.com', 'UK'], ['Grace Hopper', 'grace@example.com', 'USA'], ['Alan Turing', 'alan@example.com', 'UK'], ], styles: { fontSize: 10 }, showHead: 'everyPage', }); pdf.save('table.pdf'); }; return <button onClick={exportTable}>Export table PDF</button>;}head contains the column labels, and body contains one array per row. Font sizes are in points. Add enough rows to cross a page boundary and check that the header repeats.
Password protection and encryption limits
jsPDF exposes an encryption constructor option with userPassword, ownerPassword, and userPermissions. The supported permission names are print, modify, copy, and annot-forms.
jsPDF’s encryption implementation(opens in a new tab) uses legacy 40-bit RC4. It doesn’t offer the encryptionType AES switch sometimes shown in examples. Don’t use it as modern protection for sensitive reports. PDF permission flags also depend on reader enforcement.
For a workflow that requires stronger encryption, choose a PDF processor with documented AES support and verify the exported file’s settings.
Add metadata to your PDF
In the main export handler, add this after await pdf.html(...) and before pdf.save('report.pdf'):
pdf.setProperties({ title: 'Sales report', subject: 'Quarterly numbers', author: 'Example Company', keywords: 'react, pdf, sales', creator: 'React report app',});setProperties() writes document metadata. Open the downloaded PDF’s properties to confirm the values.
Alternative: Export with html2pdf.js
html2pdf.js(opens in a new tab) wraps html2canvas and jsPDF behind a single chainable call, so you skip the manual canvas step above. Install it:
npm install html2pdf.jsReference your content with useRef, define an options object, and chain .set().from().save():
import { useRef, useState } from 'react';import html2pdf from 'html2pdf.js';
export default function PdfConverter() { const contentRef = useRef(null); const [busy, setBusy] = useState(false); const [error, setError] = useState('');
const convertToPdf = async () => { if (!contentRef.current || busy) return; setBusy(true); setError(''); const options = { filename: 'my-document.pdf', margin: 1, image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2 }, jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }, };
try { await document.fonts.ready; await html2pdf().set(options).from(contentRef.current).save(); } catch (error) { setError(error instanceof Error ? error.message : 'PDF export failed.'); } finally { setBusy(false); } };
return ( <div> <div ref={contentRef}> <h1>Hello, PDF!</h1> <p>HTML-to-PDF conversion in React with html2pdf.js.</p> </div> <button onClick={convertToPdf} disabled={busy}>{busy ? 'Generating…' : 'Convert to PDF'}</button> {error && <p role="alert">{error}</p>} </div> );}html2pdf.js renders through html2canvas, so its PDFs contain images of text. Text selection and search aren’t preserved, and CSS support has the same limits described above. Check its page-break settings(opens in a new tab) for long documents. Installing html2canvas-pro for the earlier helper doesn’t replace html2pdf.js’s bundled renderer.
Alternative: Build PDFs with @react-pdf/renderer
If you need flexbox layouts, external fonts, and vector text without html2canvas, @react-pdf/renderer(opens in a new tab) takes a different approach — you define your PDF as React components instead of converting existing HTML.
Install it:
npm i @react-pdf/rendererCreate a PDF document component:
import { Document, Page, Text, View, StyleSheet, PDFDownloadLink,} from '@react-pdf/renderer';
const styles = StyleSheet.create({ page: { padding: 40 }, title: { fontSize: 24, marginBottom: 12 }, row: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6, },});
function InvoicePdf({ items }) { return ( <Document> <Page size="A4" style={styles.page}> <Text style={styles.title}>Invoice</Text> {items.map((item, i) => ( <View key={i} style={styles.row}> <Text>{item.name}</Text> <Text>${item.price}</Text> </View> ))} </Page> </Document> );}
// Use `PDFDownloadLink` in your app to trigger a download.export default function App() { const items = [ { name: 'Widget A', price: 25 }, { name: 'Widget B', price: 40 }, ];
return ( <PDFDownloadLink document={<InvoicePdf items={items} />} fileName="invoice.pdf" > {({ loading }) => loading ? 'Generating...' : 'Download Invoice PDF' } </PDFDownloadLink> );}Tradeoffs vs. jsPDF
- Pros — Supports flexbox layouts, custom fonts through
Font.register(), and vector text without html2canvas. - Cons — You can’t convert existing HTML/DOM elements. You must rewrite your layout using React PDF’s own
<View>,<Text>, and<Image>primitives. Check font support and the generated layout for your document.
Use @react-pdf/renderer when you’re building a PDF from scratch (invoices, reports). Use jsPDF when you need to convert an existing DOM element whose styles it supports.
Comparing HTML-to-PDF libraries for React
| Tool | Input | Output and tradeoff |
|---|---|---|
jsPDF html() | Rendered HTML element | Browser export with text drawing; limited CSS fidelity and pagination control. |
| html2canvas-pro + jsPDF | Rendered HTML element | Image-based PDF; screenshot slices can split content. |
| html2pdf.js | Rendered HTML element | Image-based PDF with configurable page-break rules. |
@react-pdf/renderer | PDF-specific React components | Text-based PDF; requires a separate document layout. |
| Puppeteer or Playwright | HTML in a headless browser | Browser print rendering; requires a server or automation environment. |
| Nutrient HTML-to-PDF API | HTML and assets submitted to an API | Hosted conversion; call it from your backend to protect credentials. |
Convert and display PDFs with Nutrient
Try Nutrient HTML-to-PDF API when your React app needs conversion handled on a server. Your backend sends HTML and assets to the API and returns the generated PDF to the browser.
For viewing and editing the resulting PDF, add Nutrient Web SDK to React. The SDK provides document viewing, search, annotations, redaction, and signing. The viewer works with PDF content; it isn’t a replacement HTML/CSS rendering engine.
Start a free trial or open the viewer demo to test viewing, search, and annotations in your React workflow.
For related conversion approaches, see HTML to PDF in JavaScript, html2pdf.js, and HTML-to-PDF generation with Node.js.
FAQ
Use useRef to reference a rendered element, pass it to jsPDF’s html() method, and await rendering before calling save(). Run the export from a button handler and configure the content width and page margins.
html() reads the rendered DOM through html2canvas. Styles from an imported stylesheet can work, but unsupported CSS properties won’t reproduce correctly. A screenshot helper also has CSS limitations. Test your layout, or use a browser-based or hosted HTML conversion service.
The constructor accepts encryption with userPassword, ownerPassword, and userPermissions. Its implementation uses legacy 40-bit RC4, not AES. Use a processor with documented modern encryption support for sensitive documents.
Nutrient HTML-to-PDF API handles conversion through your backend. Nutrient Web SDK adds viewing, search, annotations, and editing to the generated PDF in React. Choose the conversion API, the viewer, or both according to your application’s needs.