html2pdf.js alternative

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.

When html2pdf.js is the better choice

html2pdf.js still makes sense in a few cases.

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; }

See 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.

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.

This page as markdown · text