html2pdf.js alternative
html2pdf.js alternative with real Chrome rendering. Get selectable text and clean page breaks instead of image PDFs, and migrate your options in minutes.
html2pdf.js is popular because it's one line: html2pdf().from(element).save(). Under the hood it screenshots the element with html2canvas and places the image into a PDF with jsPDF. That makes it quick to set up, but it inherits the limits of both libraries.
Quick answer. Send your HTML from your backend to a hosted Chromium API and get back a real PDF with vector text.
curl -X POST https://html-pdf.com/v1/pdf \
-H "Authorization: Bearer $HTML_PDF_KEY" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Hello</h1>","format":"A4"}' \
-o report.pdf
How html2pdf.js works (and where it breaks)
This section shows a typical html2pdf.js setup and the problems people usually hit with it.
html2pdf()
.set({
margin: 10,
filename: "report.pdf",
image: { type: "jpeg", quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: "mm", format: "a4", orientation: "landscape" },
pagebreak: { mode: ["css", "legacy"] },
})
.from(document.getElementById("report"))
.save();
These are the most common complaints.
- Text isn't selectable or searchable. Each page is a JPEG or PNG, so copy-paste, search, and accessibility don't work.
- Blurry or huge files. A low
scalelooks soft. A highscaleandqualitymake files large. - Content cut across pages. The page is one tall canvas split into slices. The
pagebreakoption helps by inserting spacing, but rows and lines can still split. - CSS differences. html2canvas re-implements rendering, so some modern CSS, fonts, and cross-origin images look different from the browser.
- Canvas size limits. Very long documents can exceed browser canvas limits and come out blank or truncated.
- Main thread work. Rendering large pages can freeze the tab for a while on slower devices.
When html2pdf.js is the better choice
html2pdf.js still makes sense in a few cases.
- Quick "save this card as PDF" buttons where an image-based PDF is acceptable.
- Apps that must work offline or keep data entirely in the browser.
- Prototypes, internal tools, and low-volume use where cost matters more than fidelity.
If users need to search, copy, or print crisp text, or the document is longer than a page or two, server-side Chrome rendering usually works better.
Comparison
This table compares html2pdf.js with running Chrome yourself and with the hosted API.
| html2pdf.js | Self-hosted Puppeteer/Playwright | html-pdf.com API | |
|---|---|---|---|
| Rendering | html2canvas screenshot | Real Chromium | Real Chromium (hosted) |
| Selectable text | No | Yes | Yes (vector) |
CSS page breaks, @page |
Limited | Yes | Yes |
| Headers/footers with page numbers | Manual | Yes | Yes (header_html, footer_html) |
| Runs offline in browser | Yes | No | No |
| You maintain Chrome | No | Yes | No |
| Cost | Free | Server costs | 1 credit per page |
Migrating html2pdf.js options
Each html2pdf.js option maps to a simple API field or a line of CSS.
| html2pdf.js | html-pdf.com |
|---|---|
margin: 10 (in jsPDF.unit) |
"margin": "10mm" |
margin: [top, left, bottom, right] |
"margin": {"top": "10mm", "left": "8mm", "bottom": "10mm", "right": "8mm"} |
filename: "report.pdf" |
"filename": "report.pdf" |
jsPDF.format: "a4" / "letter" |
"format": "A4" / "Letter" |
jsPDF.orientation: "landscape" |
"landscape": true |
pagebreak options |
CSS break-before, break-inside: avoid |
html2canvas.scale, image.quality |
Not needed (vector output); scale zooms content |
The html2pdf.js margin array is [top, left, bottom, right], which differs from CSS order. Check each side when you migrate.
Migration example
Here is a full migration with a small backend route and a browser button. Keep the PDF call on your backend so the API key stays secret, and never ship the key in browser code.
Server (Node)
app.post("/api/report.pdf", express.json({ limit: "5mb" }), async (req, res) => {
const r = await fetch("https://html-pdf.com/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HTML_PDF_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html: req.body.html,
format: "A4",
landscape: true,
margin: "10mm",
filename: "report.pdf",
footer_html:
'<div style="font-size:9px;width:100%;text-align:center">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
max_pages: 50,
}),
});
if (!r.ok) return res.status(502).send(await r.text());
res.set("Content-Type", "application/pdf");
res.send(Buffer.from(await r.arrayBuffer()));
});
footer_html uses Chrome's placeholder classes. It needs a bottom margin to have room, which margin: "10mm" provides.
Browser
async function savePdf() {
const el = document.getElementById("report");
const styles = [...document.querySelectorAll("style, link[rel=stylesheet]")]
.map((n) => n.outerHTML)
.join("");
const html = `<!doctype html><html><head>${styles}</head><body>${el.outerHTML}</body></html>`;
const res = await fetch("/api/report.pdf", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ html }),
});
const url = URL.createObjectURL(await res.blob());
Object.assign(document.createElement("a"), { href: url, download: "report.pdf" }).click();
URL.revokeObjectURL(url);
}
<link> stylesheets must use absolute public URLs, since the renderer can't load from localhost. If the page is already public, send {"url": "https://..."} instead. Add wait_for_expression so charts finish rendering first.
Replace pagebreak with CSS
Two CSS rules replace the pagebreak option.
.section { break-before: page; }
table tr, figure { break-inside: avoid; }
Or skip the server
If you don't want to run Chrome yourself, the hosted API does it for you. html-pdf.com is an HTML/URL to PDF API on headless Chromium. POST https://html-pdf.com/v1/pdf with {"html": "..."} or {"url": "https://..."} and a Bearer key returns a PDF as binary, base64, or a 24-hour link.
It supports formats A3 to A6, Letter, Legal and Tabloid, custom margins, headers and footers, print or screen media, and JavaScript waits.
1 credit = 1 PDF page. You get 100 free credits to start, then packs from $10 per 1,000 pages. Purchased credits never expire. AI assistants can use it through the remote MCP server.
Get a free API key · API docs · HTML to PDF in JavaScript · HTML to PDF in React
FAQ
What is the best html2pdf.js alternative?
If you need selectable text and reliable page breaks, render the HTML in real Chromium on a server. The html-pdf.com API does this with one POST https://html-pdf.com/v1/pdf call, so you don't have to maintain Chrome yourself.
Why is my html2pdf.js PDF blurry or not searchable?
html2pdf.js turns each page into an image with html2canvas, so the text is pixels, not characters. A Chrome-rendered PDF keeps text as vector text that you can select, search and print crisply.
Is there a free way to try it?
Yes. You get 100 free credits once, with no card required, and 1 credit = 1 PDF page. Failed renders cost nothing.
Can I call the API directly from the browser?
You shouldn't, because that exposes your API key. Send the HTML to your own backend route and call the API from there, as in the example above.
