# HTML to PDF in JavaScript: save a page or div as PDF

HTML to PDF in JavaScript: window.print() with print CSS, html2pdf.js and jsPDF in the browser, or a server-side API for real text PDFs.

"HTML to PDF in JavaScript" can mean two things. You might want visitors to save a page from the browser, or you might want your code to generate a PDF file. The browser options are free and need no server, but each one has a catch, so this guide covers them first and then the server-side route.

**Quick answer.** To get a real PDF file with selectable text, call the API from your backend (Node 18+) and keep the key on the server:

```js
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: "<h1>Hello</h1>" }),
});
const pdf = Buffer.from(await r.arrayBuffer());
```

## Option 1: window.print() and print CSS (no library)

This option lets visitors save any page as a PDF with zero dependencies. Every browser can already save to PDF, so a button that opens the print dialog takes one line.

```html
<button onclick="window.print()">Save as PDF</button>
```

The user picks **Save as PDF** as the destination. Use print CSS to control what ends up on paper.

```css
@media print {
  nav, footer, .no-print, button { display: none !important; }
  body { margin: 0; }
  .card, tr { break-inside: avoid; }
  h2 { break-before: page; }
}
@page { size: A4; margin: 15mm; }
html { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
```

To print **only one div**, hide everything else while printing.

```css
@media print {
  body * { visibility: hidden; }
  #invoice, #invoice * { visibility: visible; }
  #invoice { position: absolute; left: 0; top: 0; width: 100%; }
}
```

- **Pros:** free, real selectable text, and the browser's own layout engine.
- **Cons:** the user must click through a dialog and you can't set the filename. Headers, footers and background graphics depend on the user's dialog settings, and you never get the file in your code.

## Option 2: html2pdf.js (one-click download in the browser)

This option gives visitors a direct download button without a print dialog. html2pdf.js combines html2canvas (which draws the element onto a canvas) and jsPDF (which writes the PDF).

```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<div id="invoice"><h1>Invoice #042</h1><p>Total: $120.00</p></div>
<button id="dl">Download PDF</button>
<script>
  document.getElementById("dl").addEventListener("click", () => {
    html2pdf()
      .set({
        margin: 10,
        filename: "invoice.pdf",
        image: { type: "jpeg", quality: 0.95 },
        html2canvas: { scale: 2 },
        jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
        pagebreak: { mode: ["css", "legacy"] },
      })
      .from(document.getElementById("invoice"))
      .save();
  });
</script>
```

The catch is that the PDF contains a **picture** of your HTML. Text isn't selectable or searchable, files get large, and things can blur when zoomed. Content can also be sliced in half at page edges.

html2canvas re-implements CSS, so some properties render differently from the browser. Cross-origin images also need CORS headers.

## Option 3: jsPDF directly

This option produces real text in the browser for simple documents. jsPDF builds a PDF from drawing commands.

```js
import { jsPDF } from "jspdf";

const doc = new jsPDF({ unit: "mm", format: "a4" });
doc.setFontSize(18);
doc.text("Invoice #042", 15, 20);
doc.setFontSize(11);
doc.text("Total: $120.00", 15, 30);
doc.save("invoice.pdf");
```

That works for simple documents, but you position everything by hand because it doesn't understand your HTML layout. Its `doc.html()` method uses html2canvas under the hood, with the same trade-offs as html2pdf.js.

## Option 4: generate the PDF on the server

This option gives you a real file with selectable text and Chrome-accurate layout. Use it when you need to email, store or attach the PDF. With Node.js that usually means [Puppeteer](https://html-pdf.com/puppeteer-html-to-pdf), which means running and maintaining Chrome yourself.

Or call an API that runs Chromium for you. Put the call in your backend so your key never reaches the browser.

```js
// server.mjs (Node 18+, Express)
import express from "express";

const app = express();
app.use(express.json({ limit: "5mb" }));

app.post("/api/pdf", 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,   // in production, render your own template instead of trusting the client
      format: "A4",
      margin: "15mm",
      max_pages: 10,
    }),
  });
  if (!r.ok) return res.status(r.status).json(await r.json());
  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", 'attachment; filename="invoice.pdf"');
  res.send(Buffer.from(await r.arrayBuffer()));
});

app.listen(3000);
```

Then add the one-click download in the browser.

```js
async function downloadPdf() {
  const html = document.documentElement.outerHTML; // or a specific element's outerHTML
  const res = await fetch("/api/pdf", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ html }),
  });
  if (!res.ok) return alert("PDF failed");
  const url = URL.createObjectURL(await res.blob());
  const a = Object.assign(document.createElement("a"), { href: url, download: "invoice.pdf" });
  a.click();
  URL.revokeObjectURL(url);
}
```

Make sure stylesheets and images in that HTML use absolute `https://` URLs, since the renderer can't reach `localhost`. Sending the whole page from the client is fine for a demo. In production, build the HTML on the server from your own template so users can't inject arbitrary content.

## Which one should you use?

This table matches each need to the simplest option that covers it.

| Need | Use |
|---|---|
| Let visitors save a page, no code | `window.print()` + print CSS |
| Quick client-side download, appearance over quality | html2pdf.js |
| A few lines of text, fully in the browser | jsPDF |
| Real text, accurate CSS, file in your backend | Headless Chrome on the server, or the API |

## Get an API key

One request creates an account and returns your key.

```bash
curl -X POST https://html-pdf.com/v1/signup -H "Content-Type: application/json" -d '{"email": "you@example.com"}'
```

Open the link we email you to confirm your address. That adds 100 free credits with no card. 1 credit = 1 PDF page, and failed renders cost nothing.

More: [HTML to PDF in Node.js](https://html-pdf.com/html-to-pdf-nodejs) (Express and Next.js) · [page breaks](https://html-pdf.com/html-to-pdf-page-breaks) · [API docs](https://html-pdf.com/docs)

## FAQ

### How do I convert HTML to PDF in JavaScript?

In the browser, call `window.print()` and let the user pick "Save as PDF", or use html2pdf.js for a one-click download. For a real file with selectable text, send the HTML from your backend to `POST https://html-pdf.com/v1/pdf`.

### Why is the text in my html2pdf.js PDF not selectable?

html2pdf.js renders your element to a canvas image and puts that image in the PDF. To get real, searchable text, use print CSS with `window.print()` or render with headless Chrome on the server.

### Can I call the PDF API directly from the browser?

You shouldn't, because that would expose your API key to every visitor. Put the call in a small backend route and have the browser download the PDF from there.

### Is there a free way to try the API?

Yes. New accounts get 100 free credits once, with no card, and 1 credit is 1 PDF page. Paid packs start at $10 for 1,000 credits, and purchased credits never expire.
