ScreenshotNeo

BlogHTML to image & PDF

How to Prevent Wkhtmltopdf Thead Overlap Across Pages

Fix wkhtmltopdf table headers that overlap rows across page breaks with a minimal reproduction, print CSS, diagnostics, and migration guidance.

By the ScreenshotNeo team1 October 20266 min read

Direct answer: reduce the document to one table, preserve display: table-header-group when the header must repeat, and test the table without nested tables, flex wrappers, or print-time overflow clipping. If repetition is optional, try thead { display: table-row-group; }; this can remove the overlap by disabling repeated headers. If repetition is required, use break-inside: avoid and page-break-inside: avoid on the header, then inspect every page boundary. These are diagnostic fixes rather than a universal wkhtmltopdf solution.

Minimal reproduction first

Do not debug the full application document initially. Create a small file that uses the same wkhtmltopdf build, page size, margins, fonts, and command-line flags as production. It must contain enough rows to cross a PDF page boundary.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    @page { size: A4; margin: 18mm 14mm 18mm; }
    body { font: 12px Arial, sans-serif; color: #222; }
    table { width: 100%; border-collapse: collapse; }
    th, td { border: 1px solid #999; padding: 7px; text-align: left; }
    thead {
      display: table-header-group;
      break-inside: avoid;
      page-break-inside: avoid;
    }
    tr { page-break-inside: avoid; }
  </style>
</head>
<body>
  <h1>Invoice lines</h1>
  <table>
    <thead>
      <tr><th>Item</th><th>Description</th><th>Amount</th></tr>
    </thead>
    <tbody>
      <!-- Repeat enough rows to force several pages. -->
      <tr><td>001</td><td>Example line item with enough text to wrap.</td><td>$12.00</td></tr>
      <tr><td>002</td><td>Another line item.</td><td>$18.00</td></tr>
      <tr><td>003</td><td>Copy this row many times in your fixture.</td><td>$24.00</td></tr>
    </tbody>
  </table>
</body>
</html>
wkhtmltopdf --page-size A4 --margin-top 18mm --margin-right 14mm --margin-bottom 18mm --margin-left 14mm input.html output.pdf

Compare the minimal output with production. If the overlap disappears, add the original wrappers and CSS back one change at a time.

Choose the header behavior you actually need

When the header does not need to repeat

Test this workaround:

thead { display: table-row-group; }

Issue reports describe this as removing the duplicate-header behavior. The trade-off is that the header will not repeat on later pages.

When the header must repeat

Keep table-header behavior and add break-avoidance rules:

thead {
  display: table-header-group;
  break-inside: avoid;
  page-break-inside: avoid;
}

This expresses the intended layout, but reports show that different wkhtmltopdf builds and documents can still produce orphaned headers or awkward splits. Render and inspect representative page boundaries.

Inspect the surrounding layout

Nested tables

Move the affected table out of nested tables for the reproduction. A report against wkhtmltopdf 0.12.1 describes overlapping headers with nested tables; another older build was associated with blank or corrupted pages. Simplifying the nesting is a useful isolation step.

Flex containers used during print

Temporarily change the root print container from flex to block:

@media print {
  .page-root {
    display: block;
  }
}

A commenter reported that this fixed one overlap case. Treat it as a document-specific lead and verify it against your fixture.

Overflow wrappers

Responsive wrappers often use overflow: auto or overflow: hidden. Remove clipping around the table while printing:

@media print {
  .table-wrapper {
    overflow: visible;
  }
}

This was suggested in the issue discussion as another investigation path. It is not guaranteed to fix every build.

A repeatable debugging procedure

  1. Record the exact wkhtmltopdf version and complete production command.
  2. Reduce the page to one table and enough rows for at least two pages.
  3. Confirm whether the problem is repetition, a split row, clipping, or a second header painted over the first row.
  4. Test table-row-group to determine whether repeated headers are the trigger.
  5. Restore table-header-group, then test break-avoidance rules.
  6. Remove nested tables, flex wrappers, and overflow clipping one at a time.
  7. Test long and short header text, wrapped cells, wide tables, and rows containing images.
  8. Inspect the first page after every page break, not only the final page.

Common symptoms, causes, and fixes

Symptom Likely investigation Fix to test
Repeated header covers the first body row Header repetition or a layout wrapper is being painted twice Test table-row-group; if repetition is required, restore table-header-group and remove flex/overflow wrappers
Header appears only once The header was changed to a row group Use display: table-header-group when repetition is required
Overlap occurs only inside a component Nested table or component wrapper Flatten the table for the PDF template and reintroduce nesting gradually
Blank or corrupted pages Older renderer behavior or complex nesting Reproduce with a simpler table and compare the exact renderer build
First row is orphaned after a break Pagination rules are not honored consistently Apply break avoidance, reduce header height, and inspect the generated PDF at each boundary
Rows disappear behind a container Print-time overflow clipping Set the table wrapper to overflow: visible in print

What wkhtmltopdf can and cannot guarantee

The wkhtmltopdf repository was archived on January 2, 2023, so its issue archive is read-only. That does not establish the status of every fork or downstream package. A project member described some behavior as part of patched Qt that could not be controlled by an option; treat that as historical context for the reported case, not proof that every overlap has one cause.

Standard declarations such as page-break-inside and table-header-group express intent but do not guarantee identical pagination across all builds and documents. Keep a fixture in your deployment checks if exact page layout matters.

Performance and reliability considerations

  • Use the smallest reproduction that still crosses a page boundary; it shortens each render-debug cycle.
  • Keep production page settings in the fixture. Changing margins or page size can move the break and hide the defect.
  • Test the longest real header and the widest real cell content, because wrapping changes the available space at the break.
  • Validate every page boundary after CSS changes. A fix for one break can move the problem to another.
  • If the issue remains production-critical, compare another renderer using the same HTML fixture. The comparison should cover repeated headers, page breaks, nested structures, required CSS fidelity, and deployment and maintenance requirements.

Or skip the browser setup

For website screenshots or PDF capture without maintaining a local browser renderer, ScreenshotNeo provides a GET API and an MCP server. Before capture it accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. See the ScreenshotNeo API documentation for PDF options and configuration.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

It also supports full-page capture, element selectors, custom CSS and JavaScript, waits, request blocking, headers and cookies, device and viewport settings, caching, signed links, asynchronous jobs, bulk capture, and PDF controls. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Will page-break-inside: avoid always stop the overlap?

No. Reports show that wkhtmltopdf can still split or orphan content depending on the build and document structure.

Should I remove display: table-header-group?

Only when repeated headers are unnecessary. Removing it can hide the overlap by stopping repetition.

Is the bug caused by CSS or by wkhtmltopdf?

Either is possible. The issue archive contains reports involving patched Qt behavior, nested tables, flex layout, and overflow wrappers, so isolate each factor with a minimal fixture.

Should I switch to headless Chrome?

Evaluate it with the same HTML and compare page breaks, repeated headers, fonts, deployment requirements, and maintenance. The available evidence does not establish a universal winner.