Why people leave wkhtmltopdf
Three reasons come up, none of them about features you are missing today:- The project is archived. Development ended and the GitHub repository was archived in 2023. There will be no more releases.
- Open CVEs will stay open. Known vulnerabilities (for example CVE-2022-35583, an SSRF in how it fetches resources) have no patch coming, because nothing is coming. If you feed it any user-influenced HTML, that is your problem to mitigate now.
- The engine is a patched QtWebKit from the early 2010s. It predates flexbox as you know it, CSS grid, and a decade of CSS. Every layout trick in a wkhtmltopdf template is a workaround for that engine, which is why the templates look the way they do.
The short version
What moves over unchanged
The wkhtmltopdf-era CSS dialect is, deliberately, well inside Forme’s subset:- The legacy
page-break-*spellings work.page-break-before,page-break-after,page-break-inside: avoidare honored as-is, alongside the modernbreak-*forms. <thead>repetition is automatic — the header-rows-on-every-page behavior wkhtmltopdf approximated is engine-native here.- Floats lay out as columns. The wkhtmltopdf-era float layouts (left/right header pairs, floated column grids) render as real columns.
- The cascade, as written. Type/class/id selectors, compounds, descendant and child combinators, attribute selectors (
[class*="col"]and friends),!important— the stylesheet applies the way it applies in a browser. The selector table lists the whole set. @pageand real running headers/footers — see the next section, because this is where the flags go.
Replacing the flags
Most of wkhtmltopdf’s command-line surface was compensating for missing print CSS. The replacements are CSS:
And the one idiom nearly every wkhtmltopdf template carries: the
position: fixed footer works, with these semantics. #footer { position: fixed; bottom: 0 } sits flush at the bottom of the page it occurs on — fixed anchors like absolute, because a paged renderer’s viewport is the page. It does not repeat on every page the way wkhtmltopdf repeated it; for a footer on every page, use an @page margin box (that is what they are for). A render-time warning names this difference whenever position: fixed appears.
What doesn’t move, up front
- JavaScript never executes. wkhtmltopdf ran page scripts (slowly, in an old engine); Forme does not run them at all. If your template computes totals or builds rows in
<script>, that logic moves into whatever generates the HTML. - Nothing is fetched over the network. Remote images, stylesheets, and
@font-faceURLs are never downloaded. Inline images as data URIs or local files; pass stylesheets with--css; pass fonts with--font Family=path.ttf. System-font dependence becomes explicit instead of environmental. - Text does not wrap alongside a float. Floated siblings lay out as columns — which is what invoice templates actually use floats for — but a paragraph flowing around a floated image renders below it instead, with a warning. This is the one float behavior that is out, and it is much narrower than it used to be.
- The named subset gaps. Everything else outside the subset — transforms, gradients, CSS variables,
position: sticky— warns by name at render time rather than rendering differently in silence. The full subset table is the reference.
Verifying 400 templates without opening 400 PDFs
The real fear in this migration is not “will one document render” — you can look at one document. It is “will I silently break 400 of them.” That is a structural question, and there is tooling for it: pdf-testkit diffs PDFs by structure, not pixels.The measured number
This claim is measured, not promised: of 15 production templates collected from GitHub for our compat corpus — several from the wkhtmltopdf/mPDF era, custom pagination hacks included — 14 render correctly at 0.20.0, 1 degrades legibly with its cause named in warnings, 0 render broken (how it was measured). For a template population like yours, that number is the most relevant fact on this page.Who should migrate, and who shouldn’t
Migrate if your templates are documents — invoices, statements, reports, contracts — in the CSS dialect wkhtmltopdf shaped. That is the population the subset was built against, and the corpus number above is what happened when 15 of them met it cold. Don’t migrate yet if your templates depend on in-page JavaScript to build their content, or fetch resources at render time you cannot inline. Move that logic out first, or stay put until you can. What to try first: one real template, one command, read the warnings.npx @formepdf/html your-worst-template.html -o out.pdf costs a minute and tells you more than this page can. If something renders wrong without a warning naming it, that is a bug on our side — file it.