How to convert HTML to PDF in React with jsPDF

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
    TL;DR

    Convert HTML to PDF in React without a server. This guide covers importing jsPDF(opens in a new tab), referencing JSX with useRef, and exporting a PDF — plus custom page sizes, 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 jsPDF convert to PDF?

    The live demo examples(opens in a new tab) demonstrate that jsPDF can render images, shapes, tables, styled text, and multilingual content as PDF. It also supports multipage output with page breaks, password protection, and annotations. Page orientation — landscape or portrait — is configurable.

    Setting up jsPDF for HTML to PDF in React

    This tutorial uses Vite(opens in a new tab) to scaffold the React project:

    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 i jspdf

    Step-by-step: Convert HTML to PDF in React

    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();

      The constructor accepts configuration options for paper size, orientation, and units. Defaults are A4, portrait, and millimeters.

      Pass an object to the constructor to override the defaults:

      const doc = new jsPDF({
      orientation: 'landscape', // or "portrait"
      unit: 'px', // "mm", "cm", "in" also supported
      format: [612, 792], // US Letter in px [width, height]
      });

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

    2. The html()(opens in a new tab) method converts an HTML element to PDF. It takes an HTML element and an options object that includes a callback property. Use the useRef()(opens in a new tab) hook to get a reference to the HTML element.

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

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

    Adding the markup

    jsPDF renders the report template below. A button click triggers the PDF generation.

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

    import SalesChart from './sales-chart.png';
    const styles = { img: { width: '100%' }, h1: { marginBottom: '1rem' } };
    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.

    Generating the PDF

    Next, wire up the button click handler in App.jsx. Import the useRef hook and the ReportTemplate component:

    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;

    Run npm run dev and click the button to generate a PDF.

    0:00
    0:00

    Adding custom fonts

    jsPDF supports 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. Then click Create to convert them. You don’t need to modify the fontName, fontStyle, or Module format fields.

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

    The font converter generates 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');

    The font name is visible in the converter before you generate the JavaScript file. Repeat this process for each additional 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 HTML-to-PDF techniques in React

    Beyond basic export, jsPDF supports plugins for tables, encryption, and styled captures.

    Capture styled components with html2canvas

    When your component relies on external CSS (flex/grid, classes, web fonts), jsPDF’s html() method ignores those styles. A common workaround is to rasterize the node with html2canvas(opens in a new tab) and embed the result as an image. Extract this into a helper so any component 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
    }

    The helper rasterizes the element, converts it to PNG, and places it on an A4 page. The output defaults to styled.pdf.

    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', 'ada@example.com', 'UK'],
    ['Grace Hopper', 'grace@navy.mil', 'USA'],
    ['Alan Turing', 'alan@bletchley.gov', 'UK'],
    ];
    const pdf = new jsPDF();
    autoTable(pdf, {
    head: [['Name', 'Email', 'Country']],
    body: dataArray,
    styles: { fontSize: 10 },
    });
    pdf.save('table.pdf');
    }

    head defines the header row, and body supplies one nested array per data line. autoTable sizes columns, paginates overflow rows, and repeats the header on every new page. The styles object applies global cell formatting — here, the font size is 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 metadata (title, subject, author, keywords, creator) into the PDF. Viewers like Acrobat display these fields and search engines can index them. doc.save("report.pdf") then streams the file to the browser as a download.

    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, custom fonts via Font.register(), and vector text out of the box. No html2canvas needed.
    • Cons — You can’t convert existing HTML/DOM elements. You must rewrite your layout using React PDF’s own <View>, <Text>, and <Image> primitives. Heavier bundle than jsPDF.

    Use @react-pdf/renderer when you’re building a PDF from scratch (invoices, reports). Use jsPDF when you want to export an existing HTML page as-is.

    Comparing HTML-to-PDF libraries for React

    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

    jsPDF handles basic HTML-to-PDF export in React, including custom fonts, encryption, and styled-component capture. When you need viewing, searching, commenting, redacting, or signing, the Nutrient React viewer works with any React project and includes 30+ tools and a customizable UI.

    For server-side automation, the DWS Processor API provides the same capabilities, and the open source MCP server lets AI assistants call those endpoints using natural-language prompts.

    Start a free trial or launch the live demo to try the viewer.

    FAQ

    What advantages does the Nutrient React viewer offer over jsPDF?

    jsPDF handles basic HTML-to-PDF export. Beyond that, its feature set is limited. The Nutrient React viewer renders PDFs with high fidelity and supports full-text search, annotation, redaction, digital signatures, form filling, and permission controls — all in one package. If your project needs more than static exports, Nutrient covers those use cases.

    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. For accurate CSS rendering, use a headless browser or the Nutrient React viewer instead.

    How do I add password protection with jsPDF?

    Since v2.5, you can pass a userPassword, an ownerPassword, and a permissions array when creating a 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 who enjoys creating responsive, scalable, and maintainable web experiences. She’s passionate about open source, web accessibility, cybersecurity privacy, and blockchain.

    Explore related topics

    Try for free Ready to get started?