Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To export a React component with html-to-image and jsPDF, capture the mounted component as a canvas, split that canvas into page-sized images, add each image to a jsPDF document, and save the PDF. The most common failures happen earlier: the component is not ready, an image or font is blocked by cross-origin rules, or the captured content exceeds browser canvas limits. This approach makes a visual, image-based PDF; it does not preserve selectable text.

How the export pipeline works

Think of the task as three separate operations: select the rendered DOM node, turn it into image data, then add that image data to a PDF. The html-to-image project provides promise-returning methods including toPng, toJpeg, toSvg, toBlob, toCanvas and toPixelData. jsPDF’s addImage API accepts image data, including data URLs and canvas elements.

Keeping these stages separate helps locate errors. A rejected capture points to the DOM or its resources. A good canvas but blank PDF points to PDF image insertion, format or coordinates. A PDF that downloads but looks wrong points to rendering or page layout.

Install the packages and wire up a React component

Install the two packages in the React project:

npm install html-to-image jspdf

Attach a ref to the exact mounted element to export. The following example waits for web fonts and in-element images, captures the node, and slices the resulting canvas into pages sized to fit an A4 portrait PDF. It is client-side code; call it from a user action after the component’s data has loaded.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { useRef, useState } from 'react';
import { toCanvas } from 'html-to-image';
import { jsPDF } from 'jspdf';

function waitForImages(node) {
  const images = Array.from(node.querySelectorAll('img'));
  return Promise.all(images.map((img) => {
    if (img.complete) {
      return img.decode ? img.decode().catch(() => {}) : Promise.resolve();
    }
    return new Promise((resolve) => {
      img.addEventListener('load', resolve, { once: true });
      img.addEventListener('error', resolve, { once: true });
    });
  }));
}

export default function Report() {
  const reportRef = useRef(null);
  const [exporting, setExporting] = useState(false);
  const [error, setError] = useState('');

  async function exportPdf() {
    const node = reportRef.current;
    if (!node || exporting) return;

    setExporting(true);
    setError('');
    try {
      if (document.fonts?.ready) await document.fonts.ready;
      await waitForImages(node);

      const canvas = await toCanvas(node, {
        backgroundColor: '#ffffff',
        pixelRatio: 2
      });
      if (!canvas.width || !canvas.height) {
        throw new Error('The captured canvas is empty.');
      }

      const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
      const pageWidth = pdf.internal.pageSize.getWidth();
      const pageHeight = pdf.internal.pageSize.getHeight();
      const pageHeightPx = Math.floor(canvas.width * pageHeight / pageWidth);
      const pageCanvas = document.createElement('canvas');
      const ctx = pageCanvas.getContext('2d');
      if (!ctx) throw new Error('Could not create a page canvas.');
      pageCanvas.width = canvas.width;
      pageCanvas.height = Math.min(pageHeightPx, canvas.height);

      for (let y = 0, page = 0; y < canvas.height; y += pageHeightPx, page++) {
        const sliceHeight = Math.min(pageHeightPx, canvas.height - y);
        pageCanvas.height = sliceHeight;
        ctx.clearRect(0, 0, pageCanvas.width, sliceHeight);
        ctx.drawImage(canvas, 0, y, canvas.width, sliceHeight,
          0, 0, canvas.width, sliceHeight);
        if (page > 0) pdf.addPage();
        const image = pageCanvas.toDataURL('image/png');
        const imageHeightMm = sliceHeight * pageWidth / canvas.width;
        pdf.addImage(image, 'PNG', 0, 0, pageWidth, imageHeightMm);
      }
      pdf.save('report.pdf');
    } catch (err) {
      console.error('PDF export failed:', err);
      setError(err instanceof Error ? err.message : 'PDF export failed.');
    } finally {
      setExporting(false);
    }
  }

  return (
    <main>
      <button onClick={exportPdf} disabled={exporting}>
        {exporting ? 'Creating PDF…' : 'Download PDF'}
      </button>
      {error && <p role="alert">{error}</p>}
      <article ref={reportRef} className="report">
        <h1>Monthly report</h1>
        <p>Put the content to export inside this element.</p>
      </article>
    </main>
  );
}

The image slices preserve the captured aspect ratio and avoid shrinking a long report to fit on one page. A page boundary can still cut through a paragraph or image because the canvas is sliced by height, not by document semantics. For polished reports, consider arranging content into deliberate page-sized sections and capturing each separately. The sample captures one component; it does not capture the whole browser window.

Adapt capture and PDF output deliberately

  • Resolution: pixelRatio: 2 creates a larger raster than a ratio of 1, which can look sharper in print but uses more memory. Reduce it for very long or complex nodes if capture fails.
  • Background: a white background makes transparent page areas predictable. Set a different color if the design requires it.
  • Format: the sample uses PNG. jsPDF can also add JPEG data; JPEG may reduce file size for photographic content, while PNG suits sharp edges and text better.
  • Page format: change jsPDF’s format or orientation for a different paper size. The slice calculation uses the actual page width and height, so it adapts to those settings.
  • Page margins: this sample fills the page. To add margins, reduce the available image width and height, and offset the image from the page edges accordingly.

Make sure the capture is ready and accessible

Run export only when the target node is mounted and its data is present. If React renders a loading state first, a click during that state may capture the wrong content. Disable the export button until the report is ready. The example waits for document fonts and ordinary img elements, but CSS background images and other external resources need their own readiness checks.

Cross-origin resources are a frequent source of failure. Browsers enforce origin restrictions on canvas access. The html2canvas FAQ explains that a cross-origin image can taint a canvas; the html2canvas options document useCORS, which defaults to false. That option belongs to html2canvas, not html-to-image, and enabling CORS behavior cannot override a remote server that does not permit it. The image server must return suitable Access-Control-Allow-Origin headers, or the asset must be served through an appropriate same-origin proxy.

Inspect the browser console and Network panel for failed image, font, stylesheet and background-image requests. Check the response headers and test access from the deployed page’s origin, not only from a local development setup. The html-to-image project embeds image and font resources during conversion and cautions that rendering an already-tainted canvas can fail.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Understand visual differences and browser limits

DOM-to-image libraries do not guarantee a pixel-for-pixel browser screenshot. The html2canvas documentation describes its output as a reconstruction from DOM information and supported styles, rather than a literal screenshot. Complex or unsupported CSS can therefore render differently. The html-to-image project uses SVG foreignObject and canvas; its README discusses browser and security limitations, including stricter Safari handling of foreignObject and a Firefox issue involving some external stylesheets. Treat those as project-specific cautions, not as guarantees about every current browser/version.

Test a small representative component first. Simplify complex styling, ensure assets have loaded, and inspect the captured image before debugging jsPDF. Long or high-resolution canvases can exceed browser dimension or memory limits. The html2canvas FAQ identifies canvas size limits as a cause of blank or clipped output; its options reference documents width, height, scale and viewport settings. Those are html2canvas settings, not direct html-to-image options, but the underlying browser canvas constraint is relevant to raster capture generally.

When raster PDFs are the wrong format

This method places page images in the PDF. Text is not a native text layer, so it will not be selectable or searchable in the usual way; the file may also be large. The html2pdf.js README describes those trade-offs for its client-side html2canvas-and-jsPDF pipeline. If search, accessibility, crisp zooming or natural text pagination matters, generate PDF text and graphics as PDF content instead of rasterizing the entire page. That is a different architecture, not an option that makes this image-based export retain text.

Other route: jsPDF’s HTML method

jsPDF also has an html method. Its documentation identifies html2canvas as an optional dependency for that method and DOMPurify when the input is an HTML string; bundlers may load these dynamically or create chunks. It can be convenient, but it still relies on html2canvas rendering constraints. It does not automatically solve blocked cross-origin assets or unsupported CSS.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is a screenshot of a page available at a URL—not a PDF export of a private, in-memory React component—ScreenshotNeo can return a screenshot or PDF with one GET request. It accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server offers screenshot and PDF tools for AI agents. This is a URL-based capture service, so it does not replace exporting a component that is not publicly reachable.

For example, the cURL request below saves a WebP screenshot of a public page. See the ScreenshotNeo documentation for API details and parameters.

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

The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is on every plan. Sign up for 1,000 free screenshots a month with no card.

Troubleshooting by symptom

The capture promise rejects

  • Inspect the rejection and browser console instead of assuming the click handler succeeded.
  • Check whether an image, font or stylesheet failed to load, or whether canvas security restrictions affect an asset.
  • Try capturing a smaller node with simpler CSS to distinguish resource issues from layout or size issues.

The captured image is blank or clipped

  • Confirm the ref points to a mounted, non-empty element, and that its data has rendered.
  • Check canvas width and height and reduce pixel ratio or divide the capture into intentional sections when it is unusually large.
  • Review console errors and browser canvas limits; increasing dimensions without considering those limits can make the problem worse.

The image looks right but the PDF is blank or misplaced

  • Verify that the data URL is non-empty and that the format passed to addImage matches the encoded image.
  • Check page dimensions, units, x/y coordinates and computed image height. The sample derives height from the canvas-to-page-width ratio.
  • Inspect each stage independently: open the captured image first, then test adding a single image to a one-page PDF.

The PDF differs from the page

  • Check whether the design relies on CSS or browser behavior unsupported by the capture method.
  • Wait for fonts and external assets, then simplify effects or styles on the export-only layout.
  • Test the actual supported browser and deployment origin; Safari and Firefox caveats in the project README are browser-specific, not universal predictions.

FAQ

Can I capture an element instead of the whole page?

Yes. Put the desired content inside a DOM element and pass that element, through its React ref, to the capture function.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Will the PDF text be searchable?

Not with the raster-image pipeline shown here. It embeds captured pixels rather than a native text layer.

Does enabling CORS in the capture library fix every remote image?

No. The remote server must allow the page’s origin, or the resource must be made available through a suitable same-origin route.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.