jsPDF alternative for HTML to PDF
jsPDF alternative for HTML to PDF. html2canvas output is blurry, unselectable images. See when jsPDF is fine and how to get real vector PDFs.
jsPDF is a solid library for drawing PDFs in the browser. The trouble starts when you use it to convert an existing HTML page, usually through doc.html() or html2canvas.
This page explains why those PDFs look wrong, when jsPDF is still right, and how to render HTML with Chrome instead.
Quick answer. Send the HTML from your backend to a hosted Chromium API and you get a vector PDF with selectable text.
curl -X POST https://html-pdf.com/v1/pdf \
-H "Authorization: Bearer $HTML_PDF_KEY" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Invoice #042</h1>","format":"A4"}' \
-o invoice.pdf
Why jsPDF HTML output looks wrong
This section shows the usual jsPDF pattern and why its output suffers. The common approach is to screenshot a DOM node with html2canvas, then place that image into a jsPDF document.
import html2canvas from "html2canvas";
import { jsPDF } from "jspdf";
const canvas = await html2canvas(document.querySelector("#invoice"), { scale: 2 });
const pdf = new jsPDF({ format: "a4", unit: "mm" });
pdf.addImage(canvas.toDataURL("image/png"), "PNG", 0, 0, 210, (canvas.height * 210) / canvas.width);
pdf.save("invoice.pdf");
That approach has built-in limits.
- Image-based output. Every page is a picture. Text can't be selected, searched, or copied, and screen readers can't read it.
- Blurry text. Raising
scalesharpens the image but makes the file much bigger. - Bad page splitting. The canvas is one tall image cut into page-height slices, so lines of text and table rows get sliced in half. CSS like
break-inside: avoidisn't applied. - Big files. A few pages of PNG at 2x scale adds up fast.
- CSS is approximated. html2canvas re-implements rendering, so some CSS and cross-origin images render differently or not at all.
doc.html() uses html2canvas under the hood, so it has the same rendering limits.
When jsPDF is the better choice
jsPDF is a good fit when you draw the document instead of converting HTML.
- Simple, code-generated documents (labels, tickets, receipts) built with
doc.text(),doc.rect(), and the autotable plugin. - Apps that must work offline or can't send data to a server.
- Fully client-side tools where the user's data should never leave the browser.
If you control every coordinate, jsPDF produces real text and small files. The problems above come from converting HTML.
Comparison
This table compares the two jsPDF approaches with Chrome rendering.
| jsPDF + html2canvas | jsPDF drawing API | Chrome rendering (html-pdf.com) | |
|---|---|---|---|
| Runs | Browser | Browser | Server (API) |
| Selectable text | No (image) | Yes | Yes (vector) |
| Uses your existing HTML/CSS | Approximated | No, you redraw it | Yes, real Chromium |
| Flexbox, grid, web fonts | Partial | N/A | Yes |
Page breaks (break-inside, @page) |
No | Manual | Yes |
| Works offline | Yes | Yes | No |
| Cost | Free | Free | 1 credit per page |
The alternative: render HTML with Chrome on the server
This section shows how to replace the html2canvas step with real Chrome rendering. Chrome's print engine produces vector PDFs with selectable text, follows @page and break rules, and supports modern CSS.
You can run headless Chrome yourself (Puppeteer, Playwright) or call an API that does it for you.
With html-pdf.com, your frontend calls your own backend route, and the backend calls the API. Never put the API key in browser code. Anyone could read it from the bundle and spend your credits.
Backend route (Node + Express)
import express from "express";
const app = express();
app.use(express.json({ limit: "5mb" }));
app.post("/api/pdf", async (req, res) => {
const { html } = req.body; // build or validate this server-side in real apps
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, format: "A4", margin: "15mm", max_pages: 20 }),
});
if (!r.ok) return res.status(502).send(await r.text());
res.set("Content-Type", "application/pdf");
res.send(Buffer.from(await r.arrayBuffer()));
});
app.listen(3001);
React download button
export function DownloadPdf({ targetId }) {
async function download() {
const el = document.getElementById(targetId);
const css = [...document.styleSheets]
.map((s) => { try { return [...s.cssRules].map((r) => r.cssText).join("\n"); } catch { return ""; } })
.join("\n");
const html = `<!doctype html><html><head><style>${css}</style></head><body>${el.outerHTML}</body></html>`;
const res = await fetch("/api/pdf", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ html }),
});
if (!res.ok) throw new Error("PDF failed");
const url = URL.createObjectURL(await res.blob());
Object.assign(document.createElement("a"), { href: url, download: "document.pdf" }).click();
URL.revokeObjectURL(url);
}
return <button onClick={download}>Download PDF</button>;
}
In Vue the same download() function works inside a method or <script setup>.
For production, prefer rendering the HTML on the server from your data (see HTML to PDF in React) so users can't send arbitrary markup. Images must use absolute https:// URLs or data URIs, because the renderer can't reach localhost.
Fixing page breaks
Chrome paginates the document, so you control breaks with plain CSS.
tr, .card { break-inside: avoid; }
.new-page { break-before: page; }
@page { size: A4; margin: 15mm; }
More in HTML to PDF page breaks.
Mapping jsPDF options
Your jsPDF settings map to a few API fields.
| jsPDF | html-pdf.com |
|---|---|
new jsPDF({ format: "a4" }) |
"format": "A4" |
orientation: "landscape" |
"landscape": true |
pdf.save("file.pdf") |
"filename": "file.pdf" |
Margins in addImage coordinates |
"margin": "15mm" or {top,right,bottom,left} |
Or skip the server
If you'd rather not run Chrome at all, the hosted API handles it. html-pdf.com is an HTML/URL to PDF API running headless Chromium. Send {"html": "..."} or {"url": "https://..."} to POST https://html-pdf.com/v1/pdf and get a PDF back as binary, base64, or a 24-hour link.
Options cover format, margins, landscape, headers and footers with page numbers, wait_for_expression for JavaScript-rendered pages, and max_pages.
Pricing: 1 credit = 1 PDF page. You get 100 free credits, then packs from $10 for 1,000 pages down to $0.005 per page. Purchased credits never expire. AI assistants can use the remote MCP server.
Get a free API key · API docs · HTML to PDF in JavaScript · Templates
FAQ
Why is my jsPDF PDF blurry?
jsPDF with html2canvas or doc.html() turns your HTML into an image. Raising scale helps a little but makes files bigger. Rendering with Chrome gives you vector text that stays sharp at any zoom.
Can jsPDF keep text selectable when converting HTML?
Not through html2canvas, because each page is a picture. You get selectable text only when you draw with jsPDF's own API, or when you render the HTML with Chrome.
Is there a free jsPDF alternative for HTML to PDF?
html-pdf.com gives you 100 free credits once with no card, and 1 credit = 1 PDF page. Self-hosted Puppeteer or Playwright are free too, but you run the Chrome servers yourself.
Does Chrome rendering support CSS page breaks?
Yes. break-inside: avoid, break-before: page and @page rules all work. Chromium also supports CSS @page margin boxes (Chrome 131+).
