# 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.
```bash
curl -X POST https://html-pdf.com/v1/pdf \
-H "Authorization: Bearer $HTML_PDF_KEY" \
-H "Content-Type: application/json" \
-d '{"html":"
Hello
","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.
```js
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 `scale` looks soft. A high `scale` and `quality` make files large.
- **Content cut across pages.** The page is one tall canvas split into slices. The `pagebreak` option 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)
```js
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:
'
Page of
',
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
```js
async function savePdf() {
const el = document.getElementById("report");
const styles = [...document.querySelectorAll("style, link[rel=stylesheet]")]
.map((n) => n.outerHTML)
.join("");
const html = `${styles}${el.outerHTML}`;
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);
}
```
`` 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.
```css
.section { break-before: page; }
table tr, figure { break-inside: avoid; }
```
See [HTML to PDF page breaks](https://html-pdf.com/html-to-pdf-page-breaks).
## 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](https://html-pdf.com/pdf-mcp-server).
[Get a free API key](https://html-pdf.com/login) · [API docs](https://html-pdf.com/docs) · [HTML to PDF in JavaScript](https://html-pdf.com/html-to-pdf-javascript) · [HTML to PDF in React](https://html-pdf.com/html-to-pdf-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.