Tailwind CSS Meets React: A Guide to Crafting Stylish PDFs

Tailwind CSS Meets React: A Guide to Crafting Stylish PDFs

Tuesday, March 19, 2024

Titouan Launay

I'm a software engineer and entrerepeneur, co-founder of Onedoc. I'm passionate about design, AI and the future of work.

As a developer, creating PDFs seems like a simple task. We build frontends with modern toolkit in just hours, how could static documents be any harder?

PDF was invented in 1993 by Adobe as a cross-platform document format. The format itself focuses on being portable rather than interactive - an orthogonal approach to HTML and CSS. While the latter defines a box model, the former has an imperative approach. In a nutshell, an HTML rectangle is a set of 4 lines in PDF.

This approach is the strength of PDF and the reason it is so widely used: it always looks the same. HTML on the other hand relies on many factors from screen size to browser version. What if we could bring the layout capacities of HTML to PDF?

Create your first PDF with React and Tailwind

The open-source library react-print-pdf brings a set of components and wrappers we can use to build beautiful PDFs in minutes.

Compile to HTML

The challenge with Tailwind is that it is a dynamic CSS framework. It relies on a JavaScript runtime to generate the final CSS. This is a problem for PDF generation, as we require a static file. We will first convert to static HTML, then to PDF.

For this, we will use the <Tailwind> component from react-print-pdf. This component will detect all the Tailwind classes and generate the final CSS. We can then use the compile function to convert the React tree to HTML.

1
import { Tailwind, Footnote, compile } from "@onedoc/react-print";
2
3
const getHTML = async () => {
4
return compile(
5
<Tailwind>
6
<h1 className="text-4xl font-bold">
7
Invoice #123
8
<Footnote>
9
<p className="text-sm">This is a demo invoice</p>
10
</Footnote>
11
</h1>
12
</Tailwind>
13
);
14
};

When calling getHTML, we will get the following HTML:

1
<!doctype html>
2
<html>
3
<head>
4
<style>
5
.text-4xl {
6
font-size: 2.25rem;
7
line-height: 2.5rem;
8
}
9
.font-bold {
10
font-weight: 700;
11
}
12
.text-sm {
13
font-size: 0.875rem;
14
line-height: 1.25rem;
15
}
16
</style>
17
</head>
18
<body>
19
<h1 class="text-4xl font-bold">
20
Invoice #123
21
<div class="footnote">
22
<p class="text-sm">This is a demo invoice</p>
23
</div>
24
</h1>
25
</body>
26
</html>

Converting the HTML to PDF

There are several ways to convert this HTML to a PDF:

  • Use Onedoc as a client-side or server-side API, that will support all features such as headers, footers, and page numbers.
  • If on the client side, you can use react-to-print to use the browser’s print dialog. This is cheap option but will not support advanced features and may introduce a lot of visual bugs.
  • Use a server-side headless browser such as puppeteer to convert the HTML to PDF. This is the most reliable free option, but requires a server. If you need to use it in production, we recommend you use Gotenberg.

Here is an example on how to convert the HTML to PDF using Onedoc:

1
import { Onedoc } from "@onedoc/client";
2
import { getHTML } from "./getHTML";
3
import fs from "fs";
4
5
const onedoc = new Onedoc(process.env.ONEDOC_API_KEY);
6
7
const { file, error } = await onedoc.render({
8
html: await getHTML(),
9
});
10
11
if (error) {
12
throw new Error(error);
13
}
14
15
fs.writeFileSync("invoice.pdf", new Buffer(file));

That’s it! You now have a beautiful PDF invoice generated from your React app. You can use most of Tailwind features as well as the components from react-print-pdf to create advanced layouts. Check out the documentation for more information.

Under the hood: dynamic styles

If you have made it this far, you may be wondering how the dynamic styles are converted to static CSS. Tailwind brings a specific set of challenges as it is a utility-first framework. Let’s have a look at the usual Tailwind generation process:

  1. Tailwind parses the files specified in your tailwind.config.js and generates a set of classes.
  2. It then uses a PostCSS plugin to replace the classes in your CSS with the actual styles.
  3. It then uses a JavaScript runtime to generate the final CSS, deduplicating and minifying it.

This works fine as a build tool, but bringing just-in-time compilation to PDF generation is a challenge. On serverless and browser environments, there is no file system and we need to detect dynamic classes in the React tree.

The approach that the Tailwind component uses is similar to the one in react-email. The Tailwind React component parses its children tags and detects the classes. It then uses a browserified version of Tailwind to generate the final CSS. This is then injected in the HTML. Not as easy as it sounds!

Also on our blog