DEV Community

Titouan Launay for Fileforge

Posted on • Edited on

Create PDFs with Tailwind

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.

import { Tailwind, Footnote, compile } from "@fileforge/react-print";

const getHTML = async () => {
  return compile(
    <Tailwind>
      <h1 className="text-4xl font-bold">
        Invoice #123
        <Footnote>
          <p className="text-sm">This is a demo invoice</p>
        </Footnote>
      </h1>
    </Tailwind>
  );
};
Enter fullscreen mode Exit fullscreen mode

When calling getHTML, we will get the following HTML:

<!doctype html>
<html>
  <head>
    <style>
      .text-4xl {
        font-size: 2.25rem;
        line-height: 2.5rem;
      }
      .font-bold {
        font-weight: 700;
      }
      .text-sm {
        font-size: 0.875rem;
        line-height: 1.25rem;
      }
    </style>
  </head>
  <body>
    <h1 class="text-4xl font-bold">
      Invoice #123
      <div class="footnote">
        <p class="text-sm">This is a demo invoice</p>
      </div>
    </h1>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

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:

import { FileforgeClient } from "@fileforge/client";
import { getHTML } from "./getHTML";
import fs from "fs";

const ff = new FileforgeClient({
  apiKey: process.env.FILEFORGE_API_KEY
});

const { file } = await ff.pdf.generate(await getHTML());

file.pipe(fs.createWriteStream("invoice.pdf");
Enter fullscreen mode Exit fullscreen mode

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!

This blog post was originally posted on the Fileforge blog.

Top comments (3)

Collapse
 
vens_karr_9841dc6922ac73b profile image
Vens Karr • Edited

Het genereren van PDF's met Tailwind klinkt als een handige aanpak voor ontwikkelaars. Als je echter vaak met grote documenten werkt en snel samenvattingen nodig hebt, kan een tool zoals ai samenvatting een geweldige aanvulling zijn. Het helpt je om belangrijke informatie uit PDF-bestanden te halen zonder dat je alles handmatig hoeft door te nemen. Dit kan vooral nuttig zijn voor projecten waarbij efficiëntie cruciaal is. Wat vinden jullie van het combineren van Tailwind voor PDF-ontwerp en tools zoals deze voor contentbeheer?

Collapse
 
sampsonprojects profile image
Sampson Crowley

Tailwind does not require JS to be generated any more than react... It requires JS for JIT compilation

You can just use the full framework with all of the classes. This is even already the default in development. The classes aren't stripped and minified until production build

Collapse
 
titou325 profile image
Titouan Launay

This is partly true. The main focus here is handling user-generated content and themes, and this requires JIT compilation as you can't rely on your build step to provide the needed classes - even if using the whole bundle as you will still miss dynamic syntax classes such as margin-[1cm].

There are other approaches and you can also pass along your whole css bundle if you don't need to support dynamic components :)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.