Generate a PDF in React

PDFs have given the world a common format for sharing documents and media in a way that is highly compatible among a wide variety of devices, but it can often be tricky to generate them programmatically.

We’re going to explore some options for how we can generate a PDF using JavaScript in different environments.

Table of Contents

YouTube Preview
View on YouTube

The trouble with generating PDFs…

When consuming PDFs, you’re often left reading or reviewing them almost like an image, but if you’ve ever tried to copy some text, search through the PDF, or click a link, you might have noticed that a PDF can be more than just a static image.

A lot of the solutions for generating PDFs lean on being able to generate them as an image, which lacks the accessibility and usability that embedded text provides.

But depending on your constraints and environments, perhaps its worth the tradeoff.

html2pdf.js

html2pdf.js is a clientside library that allows you to render PDFs from HTML using Canvas, particularly html2canvas and jsPDF.

To create your PDF, you specify which element you’d like to render from the page, pass it into an html2pdf instance, and the library will generate the PDF and prompt your user to download it.

How to get started?

You can include html2pdf.js in a few different way including a script tag pointing to 3rd party CDN or by importing it via npm.

If using npm, first install html2pdf.js:

npm install html2pdf.js

Import it as a dependency:

import html2pdf from 'html2pdf.js';

Select an HTML element and pass it to html2pdf:

html2pdf(document.getElementById('my-id'));

This will prompt your user to start downloading the file.

You could also import the dependency dynamically where supported to avoid it being directly bundled with your application and only loaded it as needed.

const html2pdf = await require('html2pdf.js');
html2pdf(document.getElementById('my-id'));

Lots of options available in the docs.

As an example, on a page where I was displaying an invoice, html2pdf.js would render:

Generating a PDF from HTML with html2pdf.js

What’s good about it?

html2pdf allows you to generate your PDFs right in the browser using JavaScript. This means you don’t need to deal with making a request to an external server, so you’re dealing with less infrastructure and less network requests.

You can also manage your HTML however you’d like using whatever tools, as ultimately you’ll be passing in a DOM node to render.

This also promotes the reusability of pages you’re already building, so you don’t have to maintain multiple pages separately for a UI and the PDF version.

What could be better?

Generally it works pretty well, but the rendering can be a bit inconsistent in generating the targeted HTML. The CSS might not come out perfectly, and sometimes parts of the page can appear shifted or cut off.

While the API gives you some options and flexibility, you’re still limited by how it renders the page. You can hide stuff (data-html2canvas-ignore), but you couldn’t for instance provide specific styles like you could with print previews.

The page shifts leading to items getting cut off seems to be able to be fixed with some base styles found in the html2pdf.js GitHub Issues.

@layer base { img { display: initial; } }

PDF Kit & React PDF

PDF Kit is another JavaScript HTML to PDF rendering tool that works a little bit differently.

PDF Kit itself doesn’t actually render HTML, but allows you to create and position elements using what it describes as an HTML5 Canvas-like API.

But working in a React environment you get something a bit closer to HTML or JSX, where React PDF uses a components API and PDF Kit under the hood to give a more natural way of expressing the content (just like you would in React).

We’ll focus on React PDF here, but check out the docs if you want the plain JavaScript version.

How to get started?

First install React PDF with npm:

npm install @react-pdf/renderer

Import some of the components as a dependency:

import { renderToStream, Page, Text, Document, StyleSheet } from '@react-pdf/renderer';

Set up some styles:

const styles = StyleSheet.create({
  page: { 
    padding: 50
  },
  title: {
    fontSize: 22,
  },
});

Create a document component:

const MyDocument = () => (
  <Document>
    <Page style={styles.page}>
      <Text style={styles.title}>My Text</Text>
    </Page>
  </Document>
);

And if rendering to a stream, use the renderToStream method:

await renderToStream(<MyDocument />)

At that point it works like a React component, so you can set up dynamic values to make it easier to work with dynamic data.

Creating a dynamic Router Handler

One example of using this is by create a Next.js Route Handler that queries data just like a page and renders the PDF, returning it as a stream.

To do this, you can create your route at app/pdf/route.tsx, where inside, you would create the PDF component, render it, and return it in a Next.js response.

For example:

import { NextResponse } from 'next/server';

const Invoice = (props: InvoiceProps) => (
  <Document>{/** PDF Components */}</Document>
);

export async function GET(request: Request, { params }: { params: { invoiceId: string; }}) {
  const invoice = await getMyInvoice(params.invoiceId);
  const stream = await renderToStream(<Invoice {...invoice} />)
  return new NextResponse(stream as unknown as ReadableStream)
}

Which could render:

PDF rendered with React

What’s good about it?

Rendering works pretty well.

You have options for how you render the document, including rendering it in a view and rendering it to a ReadableStream.

This can be handy like in the instance above if you wanted to create a Router Handler that dynamically generates and delivers the PDF or if you wanted to generate the PDF on a server.

It also allows you to embed actual fonts. Once opening up the PDF, the text nodes are actually selectable and copyable, which is great for a variety of reasons.

What could be better?

Currently React PDF doesn’t seem to work in the upcoming React 19. There’s been some chatter in GitHub Issues about how to support however, even a fork.

While you’re able to create PDFs in an HTML/JSX-like structure, you still have to maintain it separately with their components, though that syntax may be better than some of other APIs out there if not using using a solution like html2pdf that grabs it right from the DOM.

Puppeteer

Puppeteer is a popular option for working with HTML, taking screenshots, and trying to render dynamic things that typically involve the browser.

It’s another tool that we can use to create PDFs based on existing content.

The way it works is Puppeteer helps automate Chromium the browser, where once connected, we can navigate to different pages, interact with those pages, and as you can imagine, take screenshots and even generate PDFs.

How to get started?

Using Puppeteer is very dependent on the environment that you’re in. For instance, running Puppeteer in a serverless function is tricky, but luckily I have a tutorial for that: Build a Web Scraper with Puppeteer & Next.js API Routes.

For this example, I’ll assume that you’re able to run the standard Puppeteer library.

First, install Puppeteer:

npm install puppeteer

Import the module into your project:

import puppeteer from 'puppeteer';

Then we can automate running Puppeteer, where for instance, if we wanted to generate a PDF, we could run:

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://spacejelly.dev');
const pdf = await page.pdf();
await browser.close();

At this point, PDF is a Uint8Array which we can upload to the location of our choice.

For example:

PDF generated with Puppeteer

Tip: Check out my YouTube video where I show you how you can create an authenticated Puppeteer session with Clerk for generating PDFs!

Alternatively, instead of generating a PDF, you can take a screenshot:

const screenshot = await page.screenshot();

The difference being that .screenshot generates an image as opposed to a PDF file, which can be an important distinction.

Alt: PDF generated with Puppeteer

What’s good about it?

Puppeteer is very versatile. You have a lot of options for how you can control and interact with pages.

Once you have your page set up, you can easily capture it with the .pdf method which included embedded text which is critical to quality PDFs.

What could be better?

Puppeteer can be slow for this use case, the other methods are faster which can result in a better UX.

It can also be tricky to set up in an environment, assuming you have an environment where you can set up Puppeteer in the first place.

What’s our best option?

They all have their strengths and weaknesses, but I think React PDF is our better solution here.

Sure, we have to create and maintain a separate template, but that template doesn’t need to be identical to the UI, there are different standard and level of expectations for interactivity.

When compared to html2pdf, React PDF provides us with embedded text.

When compared to Puppeteer, React PDF is faster and easier to set up.

But ultimately you should weight the options and see what works best in your scenario.

Which one do you think works best? Let me know on Twitter!