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

```bash
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.

```js
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 `scale` sharpens 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: avoid` isn'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)

```js
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

```jsx
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](https://html-pdf.com/html-to-pdf-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.

```css
tr, .card { break-inside: avoid; }
.new-page { break-before: page; }
@page { size: A4; margin: 15mm; }
```

More in [HTML to PDF page breaks](https://html-pdf.com/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](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) · [Templates](https://html-pdf.com/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+).
