This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/how-to-convert-html-to-pdf-using-react.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. How to convert HTML to PDF in React with jsPDF and html2pdf.js

Table of contents

    This post explains how to use jsPDF to convert HTML to PDF in a React project. It covers setup, rendering, and download for reports, tickets, and similar documents.
    How to convert HTML to PDF in React with jsPDF and html2pdf.js
    Convert HTML to PDF with one API call

    Nutrient DWS handles rendering, fonts, and CSS for you — start with 50 free credits.

    TL;DR

    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:

    Terminal window
    npm create vite@latest convert-html-to-pdf -- --template react
    cd convert-html-to-pdf
    npm install
    npm install jspdf html2canvas

    Remove 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.

    0:00
    0:00

    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/.

    jsPDF font converter tool showing file upload and Create button for converting TTF to JavaScript

    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

    LimitationWhat 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:

    Terminal window
    npm install html2canvas-pro

    Create 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:

    Terminal window
    npm install jspdf-autotable

    Replace 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:

    Terminal window
    npm install html2pdf.js

    Reference 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:

    Terminal window
    npm i @react-pdf/renderer

    Create 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

    ToolInputOutput and tradeoff
    jsPDF html()Rendered HTML elementBrowser export with text drawing; limited CSS fidelity and pagination control.
    html2canvas-pro + jsPDFRendered HTML elementImage-based PDF; screenshot slices can split content.
    html2pdf.jsRendered HTML elementImage-based PDF with configurable page-break rules.
    @react-pdf/rendererPDF-specific React componentsText-based PDF; requires a separate document layout.
    Puppeteer or PlaywrightHTML in a headless browserBrowser print rendering; requires a server or automation environment.
    Nutrient HTML-to-PDF APIHTML and assets submitted to an APIHosted 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

    How do I convert HTML to PDF in React?

    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.

    Can jsPDF use my existing CSS files?

    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.

    How do I add password protection with jsPDF?

    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.

    What does Nutrient add to a React PDF workflow?

    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.

    Hulya Masharipov

    Hulya Masharipov

    Technical Writer

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

    Explore related topics

    50 free credits Start converting HTML to PDF