ScreenshotNeo

BlogHTML to image & PDF

How to Convert a React Component to PDF with jsPDF

Export a rendered React component as a PDF with jsPDF, including multi-page layout, CSS limits, fonts, images, troubleshooting, and alternatives.

By the ScreenshotNeo team30 September 20269 min read

How to Convert a React Component to PDF with jsPDF

Direct answer: attach a React ref to the component you want to export, create a jsPDF document in a user-triggered handler, pass the referenced DOM node to doc.html(), and save the PDF from its completion callback. jsPDF’s HTML renderer uses html2canvas, so it reconstructs the visual result from the DOM rather than printing the page exactly.

1. Install jsPDF

Install jsPDF in the React application that renders the component:

npm install jspdf
# or
pnpm add jspdf
# or
yarn add jspdf

The import syntax used by current jsPDF documentation is:

import { jsPDF } from 'jspdf';

Check the API for the exact release installed in your project before relying on optional renderer settings: jsPDF documentation.

2. Minimal React export

The ref must point to an element that exists after React has rendered. Keep only the document content inside that element so buttons, navigation, and transient controls do not appear in the PDF.

A React ref identifies the rendered region that jsPDF converts into a PDF.
A React ref identifies the rendered region that jsPDF converts into a PDF.
import { useRef } from 'react';
import { jsPDF } from 'jspdf';

export default function Report() {
  const reportRef = useRef(null);

  const downloadPdf = () => {
    if (!reportRef.current) return;

    const doc = new jsPDF({
      orientation: 'portrait',
      unit: 'mm',
      format: 'a4',
    });

    doc.html(reportRef.current, {
      callback: (pdf) => pdf.save('report.pdf'),
      margin: [10, 10, 10, 10],
      autoPaging: 'text',
    });
  };

  return (
    <>
      <section ref={reportRef}>
        <h1>Report</h1>
        <p>Content to export</p>
      </section>
      <button type="button" onClick={downloadPdf}>
        Download PDF
      </button>
    </>
  );
}

Save only inside the callback. Rendering is asynchronous, so calling save() immediately after doc.html() can produce an incomplete file.

3. A production-ready component

This example adds a loading state, an export-only layout, a filename, and a guard for missing content.

import { useRef, useState } from 'react';
import { jsPDF } from 'jspdf';
import './report.css';

export function Invoice({ invoice }) {
  const invoiceRef = useRef(null);
  const [isExporting, setIsExporting] = useState(false);
  const [error, setError] = useState('');

  async function downloadInvoice() {
    if (!invoiceRef.current || isExporting) return;

    setIsExporting(true);
    setError('');

    try {
      const doc = new jsPDF({
        orientation: 'portrait',
        unit: 'mm',
        format: 'a4',
      });

      await new Promise((resolve, reject) => {
        doc.html(invoiceRef.current, {
          margin: [12, 12, 12, 12],
          autoPaging: 'text',
          callback: (pdf) => {
            try {
              pdf.save(`invoice-${invoice.number}.pdf`);
              resolve();
            } catch (saveError) {
              reject(saveError);
            }
          },
        });
      });
    } catch (exportError) {
      console.error(exportError);
      setError('The PDF could not be generated. Check images, fonts, and browser console errors.');
    } finally {
      setIsExporting(false);
    }
  }

  return (
    <>
      <article ref={invoiceRef} className="pdf-document">
        <h1>Invoice {invoice.number}</h1>
        <p>Issued {invoice.issuedAt}</p>
        <dl>
          <dt>Customer</dt>
          <dd>{invoice.customerName}</dd>
          <dt>Total</dt>
          <dd>{invoice.total}</dd>
        </dl>
      </article>
      <button type="button" onClick={downloadInvoice} disabled={isExporting}>
        {isExporting ? 'Creating PDF…' : 'Download invoice'}
      </button>
      {error && <p role="alert">{error}</p>}
    </>
  );
}

4. Configure page size, orientation, units, and margins

Option Examples Use
orientation 'portrait', 'landscape' Choose the page direction.
unit 'mm', 'pt', 'px' Sets the coordinate unit used by the document.
format 'a4', 'letter', custom array Defines the paper size.
margin [10, 10, 10, 10] Top, left, bottom, and right margins for HTML rendering.
autoPaging 'text' Lets the HTML renderer flow content across pages.

Set these values deliberately. A component designed for a wide browser viewport may need an export-specific width, smaller spacing, or a landscape page.

5. Prepare the component for predictable output

Capture a dedicated document region

Use a wrapper around the report, invoice, or receipt rather than a page-level container. This prevents navigation, modals, download buttons, and live controls from entering the PDF.

<main>
  <section ref={reportRef} className="pdf-document">
    {/* Exportable content only */}
  </section>
  <button onClick={downloadPdf}>Download</button>
</main>

Use export-only CSS

.pdf-document {
  width: 180mm;
  padding: 12mm;
  background: #fff;
  color: #111;
}

@media print {
  .screen-only {
    display: none;
  }
}

The HTML renderer does not behave like a full browser print engine. Keep layouts simple, avoid relying on unsupported CSS, and inspect the generated file in each browser you support.

Wait for data and images

Do not enable the export action until asynchronous content has loaded. For images, use stable dimensions and ensure the image server permits the browser’s cross-origin request.

<img
  src={logoUrl}
  alt="Company logo"
  width="240"
  height="64"
  crossOrigin="anonymous"
/>

The crossOrigin attribute alone cannot grant permission. The image response must provide suitable CORS headers.

6. Page breaks and long documents

Long content is where DOM-to-PDF conversion needs the most review. Split large reports into logical blocks, avoid elements that must never be divided, and test tables, cards, and headings at page boundaries.

.pdf-section {
  break-inside: avoid;
  page-break-inside: avoid;
}

.pdf-page-break {
  break-before: page;
  page-break-before: always;
}

These CSS properties can help, but the html2canvas-based pipeline does not guarantee print-engine pagination. If exact page composition is a requirement, generate PDF-native content instead of converting an existing DOM tree.

7. Fonts, Unicode, and user content

jsPDF’s standard PDF fonts have limited ASCII coverage. Accented characters, Cyrillic, Arabic, CJK text, and many symbols require a custom font containing the needed glyphs. Verify that the font is loaded and embedded according to the jsPDF font workflow before exporting.

Sanitize untrusted values before inserting them into the export. The jsPDF documentation states: “We strongly advise you to sanitize user input before passing it to jsPDF!”

For user-generated HTML, render safe React text and approved elements rather than injecting unsanitized markup.

8. Images, CORS, and html2canvas limits

html2canvas rebuilds a visual representation from DOM data; it does not take a literal screenshot. Unsupported CSS may be missing or look different. Cross-origin images and other resources can be skipped or taint the canvas when the remote server does not allow access. Browser security rules cannot be bypassed by jsPDF or html2canvas.

  • Host images with appropriate CORS headers.
  • Use same-origin assets when possible.
  • Give images explicit width and height to reduce layout shifts.
  • Replace effects that are important to the document with simpler backgrounds, borders, and typography.
  • Check web fonts in the generated PDF instead of assuming browser-loaded fonts will be embedded.

9. When jsPDF HTML export is the wrong fit

Need Better direction
Reuse an existing DOM component quickly jsPDF html() with a React ref.
Client-side element-to-PDF wrapper html2pdf.js, which combines html2canvas and jsPDF and runs in a browser.
Precise PDF layout and reusable document templates PDF-native drawing methods in jsPDF or a PDF-specific React renderer.
React-defined PDF pages with server or browser rendering React PDF components, such as Document, Page, and Text.
Server-only Node.js execution from an existing browser DOM Use a server-side document-generation approach; the html2canvas path is browser-dependent.

Choose DOM capture when reusing the rendered component matters more than print fidelity. Choose a PDF-native renderer when page geometry, typography, and pagination must be deterministic.

10. Or skip the browser setup

If your input is a URL rather than a private in-memory React component, ScreenshotNeo can capture the rendered page and return a PDF through one API request. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server for Claude, Cursor, and other MCP clients.

ScreenshotNeo removes consent banners, popups, and chat widgets before capture.
ScreenshotNeo removes consent banners, popups, and chat widgets before capture.

See the ScreenshotNeo API documentation for current parameters and PDF options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -d format=pdf \
  -o page.pdf

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://stripe.com",
        "format": "pdf",
    },
    timeout=90,
)
r.raise_for_status()
open("page.pdf", "wb").write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
  format: 'pdf',
});

const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo request failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('page.pdf', data));

Use ScreenshotNeo when you need a URL capture without maintaining browser automation. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; 1,000 screenshots a month are free with no card and paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

11. Troubleshooting

Symptom Likely cause Fix
reportRef.current is null The handler runs before the component is mounted or the ref is attached to the wrong element. Attach the ref to the rendered export region and call the handler from a user action after render.
PDF is blank The node has no content yet, is hidden, or rendering failed. Wait for data and fonts, avoid display:none on the target, and inspect browser console errors.
Images are missing Cross-origin restrictions or images not loaded before capture. Use same-origin assets or configure CORS, set dimensions, and wait for image loading.
Styles differ from the page html2canvas does not support every CSS feature and reconstructs pixels from DOM data. Simplify export CSS, add an export-only layout, or switch to PDF-native rendering.
Text is clipped or overlaps Content exceeds the chosen page width, uses complex layout, or pagination split an element. Set a document width that fits the page, reduce spacing, and test page-break rules.
Non-English characters are broken The default PDF fonts do not contain the required glyphs. Embed a custom font with the necessary Unicode coverage.
Download starts before content is complete save() was called before the HTML callback. Call pdf.save() inside callback.
Works locally but fails in production Production asset URLs, CSP, CORS, or font loading differ. Check deployed network responses and security headers in the target browser.

12. Performance, reliability, and cost

Performance

  • Capture only the required subtree instead of the whole application.
  • Keep export styles and images smaller than the interactive page where possible.
  • Prevent duplicate clicks while a conversion is running.
  • Test long reports with realistic data; the sources do not provide a universal speed or size benchmark.

Reliability

  • Run export from a browser event after the component has rendered.
  • Handle failures visibly and log the original error for diagnosis.
  • Test Chrome, Firefox, and Safari if they are supported by your application.
  • Use a PDF-native workflow for contracts, invoices, or regulated documents where exact pagination is mandatory.

Cost

jsPDF, html2canvas, html2pdf.js, and React PDF are software libraries; browser-side generation does not add an API capture charge. Your application still pays normal browser CPU, memory, and bandwidth costs. ScreenshotNeo has a free plan with 1,000 shots per month and no card; paid plans start at $5 for 3,000 shots, with every feature available on every plan.

13. Checklist before shipping

  • Install and import the jsPDF version used by the project.
  • Attach a ref to only the content intended for the PDF.
  • Trigger export after data, images, and fonts are ready.
  • Set page format, orientation, units, and margins intentionally.
  • Review long content and page breaks in target browsers.
  • Verify CORS for every remote image and font.
  • Embed a Unicode font when the document contains non-ASCII text.
  • Sanitize untrusted values before rendering or passing them to jsPDF.
  • Save from the asynchronous callback and prevent duplicate exports.

14. FAQ

Can jsPDF convert any React component?

It can render a mounted DOM subtree, but the result depends on html2canvas support for the styles, images, fonts, and layout used by that subtree.

Does this work in Node.js without a browser?

No. The html2canvas path depends on browser APIs and browser security rules. Use a server-oriented PDF generator for server-only execution.

Should I use browser print instead?

Browser print can preserve print CSS and native pagination better for some documents. jsPDF is useful when the application must create and download a PDF directly from a button.

How do I export a private component that is not available at a URL?

Keep the browser-side ref and jsPDF flow. A URL screenshot API cannot access unsaved in-memory state unless you first expose the content through an accessible page.

Why does a PDF-native renderer look different?

React PDF and direct jsPDF drawing describe PDF pages directly; they do not reconstruct the existing browser DOM. That difference gives more layout control but requires a separate document layout.