Tailwind CSS Meets React: A Guide to Crafting Stylish PDFs
Tuesday, March 19, 2024
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 "@fileforge/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 Fileforge 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 Fileforge:
1 | import { FileforgeClient } from "@fileforge/client"; |
2 | import { getHTML } from "./getHTML"; |
3 | import fs from "fs"; |
4 | import { pipeline } from "stream/promises"; |
5 | |
6 | const fileforge = new FileforgeClient({ |
7 | apiKey: process.env.FILEFORGE_API_KEY, |
8 | }); |
9 | |
10 | const file = await fileforge.pdf.generate(await getHTML(), { |
11 | host: false, // Set to true to retrieve an URL rather than a file |
12 | test: true, // Set to false to remove the watermark |
13 | }); |
14 | |
15 | await pipeline(file, fs.createWriteStream("invoice.pdf")); |
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:
- Tailwind parses the files specified in your
tailwind.config.js
and generates a set of classes. - It then uses a PostCSS plugin to replace the classes in your CSS with the actual styles.
- 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 is described here. 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!