Why doesn't wkhtmltopdf support modern CSS?

Short answer: It uses an outdated Qt WebKit engine (2013) that predates modern CSS specs like flexbox and grid, so newer features simply don't exist.

wkhtmltopdf uses Qt WebKit 538.1 from 2013, which predates CSS flexbox (stable 2015) and CSS Grid (released 2017). This means any modern layout code fails silently or renders incorrectly, forcing developers into brittle table-based workarounds that break responsiveness and maintainability. The project has no plans to update the engine, making it unsuitable for modern web development.


Why does this happen?

wkhtmltopdf is built on an old fork of Qt WebKit that hasn't been updated since 2013:

  • Qt WebKit 538.1: The rendering engine frozen at a 2013 snapshot of WebKit.
  • Flexbox standardization: CSS Flexible Box Layout became a W3C Candidate Recommendation in September 2012, but wasn't fully stable and widely implemented until 2015.
  • CSS Grid: Released in March 2017 across all major browsers, years after wkhtmltopdf's engine was locked.
  • No maintenance path: The Qt WebKit project is unmaintained, and wkhtmltopdf has explicitly stated they won't upgrade the engine.

This isn't a bug—it's a fundamental architectural limitation. The tool predates the CSS features modern developers expect.

What CSS features don't work in wkhtmltopdf?

Short answer: Flexbox, Grid, CSS variables and many modern selectors either fail or behave partially, breaking modern layouts.

Here's a breakdown of CSS support in wkhtmltopdf vs modern browsers:


CSS Featurewkhtmltopdf SupportModern BrowsersImpact
Flexbox (display: flex)❌ None✅ FullLayouts break completely
CSS Grid (display: grid)❌ None✅ FullMulti-column layouts fail
CSS Variables (--custom-prop)❌ None✅ FullCan't use design tokens
calc()⚠️ Partial✅ FullComplex sizing breaks
Media queries⚠️ Limited✅ FullPrint styles inconsistent
Modern selectors (:is(), :where())❌ None✅ FullSelector logic fails
position: sticky❌ None✅ FullTable headers won't stick
Custom fonts via @font-face⚠️ Buggy✅ FullFonts may not load

The ❌ and ⚠️ markers mean your CSS either silently fails or produces unexpected results.

Why don't common workarounds scale?

Short answer: Table, float, and inline-style workarounds create brittle, unmaintainable HTML that breaks responsiveness and accessibility.

Developers typically try three approaches to work around these limitations:


1. Tables Instead of Flexbox

<!-- Modern approach (doesn't work in wkhtmltopdf) -->
<div style="display: flex; justify-content: space-between;">
  <div>Left</div>
  <div>Right</div>
</div>

<!-- wkhtmltopdf workaround -->
<table style="width: 100%;">
  <tr>
    <td style="width: 50%;">Left</td>
    <td style="width: 50%; text-align: right;">Right</td>
  </tr>
</table>

Problems:

  • Tables are semantically incorrect for layout.
  • Nested tables become unmaintainable quickly.
  • Responsive design is nearly impossible (tables don't reflow).
  • Accessibility suffers (screen readers expect tables for data).

2. Floats Instead of Grid

/* Modern approach (doesn't work) */
.container {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;
  gap: 1rem;
}

/* wkhtmltopdf workaround */
.container::after {
  content: "";
  display: table;
  clear: both;
}
.col {
  float: left;
  width: 25%;
  margin-right: 4%;
}

Problems:

  • Floats require manual clearfix hacks.
  • Vertical alignment is painful (no align-items).
  • Gap spacing requires manual margin calculations.
  • Breaks when content height varies.

3. Inline Styles Everywhere

Because external stylesheets sometimes fail to load, developers inline everything:

<div style="width: 50%; float: left; margin-right: 2%; padding: 10px; border: 1px solid #ccc; background: #f9f9f9;">
  <!-- 50 more inline styles... -->
</div>

Problems:

  • HTML becomes bloated and unreadable.
  • Can't reuse styles across documents.
  • Version control diffs are noisy.
  • Designers can't iterate without editing code.

When is wkhtmltopdf still acceptable?

Short answer: For simple, static, table-based documents with low volume and no design changes, it still works.

wkhtmltopdf is acceptable in these narrow scenarios:


  • Simple documents: Single-column text with basic formatting (headings, paragraphs, lists).
  • Legacy systems: You already have table-based HTML and can't refactor.
  • Low volume: <100 PDFs/day where maintenance cost doesn't matter.
  • No design changes: Static templates that never need updates.

If your use case involves dynamic layouts, modern CSS, or frequent design iteration, wkhtmltopdf will slow you down.

What are modern alternatives to wkhtmltopdf?

Short answer: Headless Chrome (Puppeteer/Playwright) or managed rendering APIs are modern alternatives that support current CSS.

Headless Chrome (Puppeteer/Playwright)

Pros:


  • Full modern CSS support (flexbox, grid, variables).
  • Uses the same rendering engine as Chrome browser.
  • Can render complex JavaScript-driven UIs.

Cons:

Managed Rendering APIs

Pros:

  • Template-based approach separates data from design.
  • No binary to deploy or manage.
  • Consistent performance (no memory leaks).
  • Visual editors for non-developers.

Cons:

  • Less control over raw HTML rendering.
  • API dependency (though usually more reliable than self-hosted Chrome).
  • Per-render pricing instead of flat compute cost.

Code Comparison

Before (wkhtmltopdf - table-based workaround)

<!-- invoice-wkhtmltopdf.html -->
<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; margin: 20px; }
    table { width: 100%; border-collapse: collapse; }
    td { padding: 8px; }
    .header { background: #f0f0f0; font-weight: bold; }
  </style>
</head>
<body>
  <h1>Invoice #12345</h1>
  
  <!-- Two-column layout using nested tables -->
  <table>
    <tr>
      <td style="width: 50%; vertical-align: top;">
        <table>
          <tr class="header"><td>Bill To</td></tr>
          <tr><td>John Doe<br>123 Main St<br>City, State 12345</td></tr>
        </table>
      </td>
      <td style="width: 50%; vertical-align: top;">
        <table>
          <tr class="header"><td>Invoice Details</td></tr>
          <tr><td>Date: 2025-12-29<br>Due: 2026-01-28</td></tr>
        </table>
      </td>
    </tr>
  </table>
  
  <!-- Line items table -->
  <table style="margin-top: 20px; border: 1px solid #ddd;">
    <tr class="header">
      <td>Description</td>
      <td style="text-align: right; width: 20%;">Amount</td>
    </tr>
    <tr>
      <td>Consulting Services</td>
      <td style="text-align: right;">$5,000.00</td>
    </tr>
    <tr>
      <td style="text-align: right; font-weight: bold;">Total</td>
      <td style="text-align: right; font-weight: bold;">$5,000.00</td>
    </tr>
  </table>
</body>
</html>
# Generate PDF
wkhtmltopdf invoice-wkhtmltopdf.html invoice.pdf
# Works, but HTML is painful to maintain and non-responsive

After (Modern CSS with API)

<!-- invoice-modern.html -->
<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      font-family: system-ui, sans-serif;
      margin: 2rem;
      line-height: 1.6;
    }
    
    /* Modern flexbox layout */
    .header-row {
      display: flex;
      gap: 2rem;
      margin-bottom: 2rem;
    }
    .header-col {
      flex: 1;
      padding: 1rem;
      background: #f9fafb;
      border-radius: 4px;
    }
    
    /* CSS Grid for line items */
    .line-items {
      display: grid;
      grid-template-columns: 1fr auto;
      gap: 0.5rem;
      border-top: 2px solid #e5e7eb;
      padding-top: 1rem;
    }
    .line-items .total {
      font-weight: 600;
      border-top: 1px solid #d1d5db;
      padding-top: 0.5rem;
    }
  </style>
</head>
<body>
  <h1>Invoice #12345</h1>
  
  <div class="header-row">
    <div class="header-col">
      <strong>Bill To</strong><br>
      John Doe<br>
      123 Main St<br>
      City, State 12345
    </div>
    <div class="header-col">
      <strong>Invoice Details</strong><br>
      Date: 2025-12-29<br>
      Due: 2026-01-28
    </div>
  </div>
  
  <div class="line-items">
    <div>Consulting Services</div>
    <div style="text-align: right;">$5,000.00</div>
    
    <div class="total">Total</div>
    <div class="total" style="text-align: right;">$5,000.00</div>
  </div>
</body>
</html>
// render-with-api.js
// Use a rendering API that supports modern CSS
const API_URL = 'https://api.hundredocs.com/v1/pdf'
const API_KEY = process.env.HUNDRED_DOCS_API_KEY

async function renderInvoice(html) {
  const res = await fetch(API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY
    },
    body: JSON.stringify({
      html,
      format: 'A4',
      margin: '1cm'
    })
  })
  
  if (!res.ok) {
    throw new Error(`Render failed: ${res.status}`)
  }
  
  const buffer = Buffer.from(await res.arrayBuffer())
  await fs.promises.writeFile('invoice.pdf', buffer)
}

// Usage
const html = await fs.promises.readFile('invoice-modern.html', 'utf-8')
await renderInvoice(html)
// Result: Clean HTML, modern CSS, maintainable

When should you use wkhtmltopdf vs modern tools?

Short answer: Use wkhtmltopdf only for legacy, simple, low-change documents; use modern tools for maintainability and modern CSS features.

Use wkhtmltopdf when:


  • You have legacy table-based HTML that already works.
  • Volume is <50 PDFs/day and maintenance cost is acceptable.
  • You're in a locked-down environment where you can't deploy modern tools.
  • Documents are purely text (no complex layouts).

Use Puppeteer/Playwright when:

  • You need full control over rendering (custom JS, complex interactions).
  • Volume is moderate (10-100 PDFs/day) and you can manage infrastructure.
  • You're okay with 150-300MB binary and memory leak mitigation.

Use managed APIs when:

How do you migrate away from wkhtmltopdf?

Short answer: Audit, refactor incrementally, test in parallel, and switch traffic once visual parity is confirmed.

If you're moving away from wkhtmltopdf:


  1. Audit your HTML: Identify table-based layouts and inline styles that can be replaced with flexbox/grid.
  2. Refactor incrementally: Convert one document type at a time (invoices first, then reports).
  3. Run parallel tests: Generate PDFs with both tools and compare visually (use tools like diff-pdf).
  4. Switch traffic: Once confident, deprecate wkhtmltopdf and update documentation.

Short answer: See Puppeteer alternatives, paged media challenges, JSON-to-PDF concepts, and wkhtmltopdf comparison for next steps.

Tools like Hundred Docs render PDFs using modern Chromium, so flexbox, grid, and CSS variables work as expected. Template-based workflows let designers iterate without touching code, while developers send JSON and receive PDFs through a stateless API.