HTML to PDF with Tailwind CSS

You built an invoice or report with Tailwind, it looks right in the browser, and the PDF comes out as one long column with no background colours. Nothing is broken: a printed page is much narrower than your screen, and Chromium drops backgrounds by default. Here is what we measured and the setup that prints exactly what you designed.

Why the layout collapses: breakpoints on paper

Tailwind's responsive prefixes are min-width media queries (sm 640px, md 768px, lg 1024px). When Chromium prints, they are evaluated against the page's content box (paper width minus margins, at 96 CSS px per inch), not the browser viewport. We printed a probe page through Chromium at common sizes:

Page and marginsBreakpoints that apply
A4 portrait, no margin
794px wide
sm, md
A4 portrait, 10mm or 15mm
718px / 680px wide
sm only
US Letter, 10mm
740px wide
sm only
A4 or Letter with 25mm / 1in margins
605px / 624px wide
none: mobile layout
A4 landscape, 10mm
1047px wide
sm, md, lg

So lg:grid-cols-3 on a portrait page gives one column, and a margin change alone can flip md: on or off. JavaScript is no help: during the same print, window.innerWidth still reported 1280.

Fix: design documents for the page, not the screen. Use unprefixed classes (grid-cols-3), or keep the screen layout and add print: overrides (print:grid-cols-3). Tailwind's print: variant matches every PDF render.

Why the backgrounds are missing

Puppeteer and Playwright both default page.pdf() to printBackground: false. Our bg-indigo-600 header with text-white printed as pale grey text on white. Pass printBackground: true, or add print-color-adjust: exact to the elements (in Tailwind, the arbitrary property [print-color-adjust:exact]): with it, our header kept its colour even with printBackground off. The Shotmint API prints backgrounds by default (print_background, default true).

await page.pdf({ path: 'invoice.pdf', format: 'A4', printBackground: true });

Don't print with the Play CDN

The <script src="https://cdn.tailwindcss.com"> tag (or @tailwindcss/browser@4) generates CSS in the browser at load time. It did produce correct output in our tests, but every PDF then downloads and runs a compiler from a third-party CDN, and Tailwind says the Play CDN is not meant for production. Rendering the same invoice through Chromium (warm browser, 6 runs):

CSS sourceRender time
Compiled CSS inlined in a <style> tag (7 KB)76-105 ms
Play CDN v4 (@tailwindcss/browser@4)188-212 ms
Play CDN v3 (cdn.tailwindcss.com)347-410 ms

Compile once with the Tailwind CLI and inline the result: faster, no network dependency at render time, and the HTML you send is self-contained.

1. The template

A normal Tailwind page. The layout uses unprefixed grid classes, break-inside-avoid keeps table rows whole, and break-before-page starts the terms on page 2.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="tailwind.css">
</head>
<body class="font-sans text-slate-800">
  <header class="flex items-center justify-between rounded-xl bg-indigo-600 p-6 text-white">
    <h1 class="text-2xl font-bold">Invoice #1042</h1>
    <p class="text-sm">Issued 20 Oct 2026</p>
  </header>

  <!-- grid-cols-3, not lg:grid-cols-3: an A4 page is narrower than the lg breakpoint -->
  <section class="mt-6 grid grid-cols-3 gap-4 text-sm">
    <div class="rounded-lg bg-slate-100 p-4"><p class="text-slate-500">Billed to</p><p class="font-semibold">Globex Ltd</p></div>
    <div class="rounded-lg bg-slate-100 p-4"><p class="text-slate-500">Due</p><p class="font-semibold">19 Nov 2026</p></div>
    <div class="rounded-lg bg-slate-100 p-4"><p class="text-slate-500">Amount due</p><p class="font-semibold">€1,240.00</p></div>
  </section>

  <table class="mt-8 w-full text-sm">
    <thead class="border-b-2 border-slate-300 text-left"><tr><th class="py-2">Item</th><th class="py-2 text-right">Amount</th></tr></thead>
    <tbody class="divide-y divide-slate-200">
      <tr class="break-inside-avoid"><td class="py-2">Design sprint</td><td class="py-2 text-right">€900.00</td></tr>
      <tr class="break-inside-avoid"><td class="py-2">Hosting, 12 months</td><td class="py-2 text-right">€340.00</td></tr>
    </tbody>
  </table>

  <!-- Starts on a new page in the PDF -->
  <section class="break-before-page">
    <h2 class="text-lg font-semibold">Terms</h2>
    <p class="mt-2 text-sm text-slate-600">Payment within 30 days by bank transfer.</p>
  </section>
</body>
</html>

2. Build the CSS (Tailwind v4)

npm install tailwindcss @tailwindcss/cli
echo '@import "tailwindcss";' > input.css
npx @tailwindcss/cli -i input.css -o tailwind.css --minify

Tailwind v4 scans the folder for class names, so tailwind.css holds only what invoice.html uses (7 KB here). Rebuild when the template changes, or add --watch while you work on it.

3. Inline it and render the PDF

Node 18+ (save as make-pdf.mjs, set SHOTMINT_KEY):

// node make-pdf.mjs  →  invoice.pdf
import { readFile, writeFile } from 'node:fs/promises';

const css = await readFile('tailwind.css', 'utf8');
const html = (await readFile('invoice.html', 'utf8'))
  .replace('<link rel="stylesheet" href="tailwind.css">', `<style>${css}</style>`);

const res = await fetch('https://shotmint.creasework.com/v1/pdf', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.SHOTMINT_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ html, format: 'A4', margin: '15mm' }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
await writeFile('invoice.pdf', Buffer.from(await res.arrayBuffer()));
console.log('invoice.pdf written, credits left:', res.headers.get('x-credits-remaining'));

Python (pip install requests):

# python3 make_pdf.py  ->  invoice.pdf
import os, requests

css = open("tailwind.css", encoding="utf-8").read()
html = open("invoice.html", encoding="utf-8").read().replace(
    '<link rel="stylesheet" href="tailwind.css">', f"<style>{css}</style>")

res = requests.post(
    "https://shotmint.creasework.com/v1/pdf",
    headers={"X-API-Key": os.environ["SHOTMINT_KEY"]},
    json={"html": html, "format": "A4", "margin": "15mm"},
    timeout=60,
)
res.raise_for_status()
open("invoice.pdf", "wb").write(res.content)
print("invoice.pdf written, credits left:", res.headers.get("x-credits-remaining"))

Both scripts were run against the live API: a 2-page A4 PDF with the indigo header, the three cards side by side and the terms on page 2, text selectable, 1 credit each.

Print checklist for Tailwind documents

Related: HTML to PDF invoices, HTML to PDF with charts, HTML to PDF in Node.js, free HTML to PDF converter.

What it costs

One PDF = one credit, however many pages it has. €5 buys 2,000 renders, credits never expire and there's no subscription.

Try it with 25 free renders a month

No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire). From a script, an AI agent, Make or Zapier, POST /v1/keys/free (no body) returns a key as JSON (openapi.json, no-code recipes).

Get a free API key Full API reference

More guides: HTML to PDF and HTML to image

All guides: guides · integrations · comparisons