Get started
PDF generation in Vue and Nuxt: every working option in 2026

PDF generation in Vue and Nuxt: every working option in 2026

Generate PDFs in Vue and Nuxt in 2026: client-side jsPDF, server-side Playwright in Nitro routes, and a hosted HTML-to-PDF API, with the tradeoffs.

8 min read

Generating a PDF in Vue or Nuxt comes down to one decision: where does the rendering happen. In the browser, jsPDF with html2canvas turns a component into a bitmap PDF in a few lines, but the text is an image. On the server, a Nuxt route running Playwright or Puppeteer renders real HTML to a vector PDF with selectable text. A hosted HTML-to-PDF API gives the same Chromium output without a browser in your deployment. This guide shows each working path, with the tradeoffs and code for Vue 3 and Nuxt 3.

Which PDF approach should you use in Vue or Nuxt?

The right approach depends on whether you need selectable text, how much CSS fidelity you want, and whether your deployment can run a Chromium process. The table maps each option to its constraints.

ApproachWhere it runsText selectableCSS fidelityBest for
jsPDF + html2canvasBrowserNo (bitmap)Low to mediumQuick client-side exports, offline, no server
jsPDF (manual layout)BrowserYesNone (you draw it)Simple receipts, fixed layouts, no HTML
Playwright / Puppeteer in a Nitro routeYour Node serverYesFull (Chromium)High-fidelity PDFs you host yourself
Hosted HTML-to-PDF APIExternal serviceYesFull (Chromium)Any Nitro preset, including edge, no browser to run

The split is between bitmap and vector. html2canvas paints the DOM onto a canvas and jsPDF embeds that image, so the result is a picture of your page. A browser engine keeps text as text, so the PDF is searchable and selectable and embeds fonts correctly. The rest of this guide covers each path in order.

How do you generate a PDF in Vue with jsPDF and html2canvas?

Client-side, mount the component, pass its DOM node to html2canvas to get a bitmap, then add that bitmap to a jsPDF document and save. This runs entirely in the browser, ships no server, and works offline. The cost is that the output is a rasterized image: the text is not selectable, and high-resolution captures produce large files.

<script setup lang="ts">
import { ref } from "vue";
import jsPDF from "jspdf";
import html2canvas from "html2canvas";
 
const target = ref<HTMLElement | null>(null);
 
async function exportPdf() {
  if (!target.value) return;
  const canvas = await html2canvas(target.value, { scale: 2 });
  const pdf = new jsPDF({ unit: "px", format: "a4" });
  const width = pdf.internal.pageSize.getWidth();
  const height = (canvas.height * width) / canvas.width;
  pdf.addImage(canvas.toDataURL("image/png"), "PNG", 0, 0, width, height);
  pdf.save("document.pdf");
}
</script>
 
<template>
  <div ref="target" class="invoice">...</div>
  <button @click="exportPdf">Download PDF</button>
</template>

Use this when the document is short, the layout is simple, and selectable text does not matter, such as a one-off export of a dashboard panel. Avoid it for invoices, contracts, or anything multi-page, where the rasterized text and manual page-splitting become a maintenance cost. For documents that span pages, a browser engine handles pagination for you instead.

How do you generate a PDF in Nuxt with Playwright?

In Nuxt, put the browser on the server. Nuxt server routes run on Nitro, a Node server, so a route under server/api can launch Playwright, set your HTML, and return a vector PDF. The text stays selectable, fonts embed, and print CSS controls pagination. This keeps the heavy dependency off the client bundle and out of the user's browser.

import { chromium } from "playwright";
 
export default defineEventHandler(async (event) => {
  const { html } = await readBody(event);
 
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage();
    await page.setContent(html, { waitUntil: "load" });
    const pdf = await page.pdf({ format: "A4", printBackground: true });
 
    setHeader(event, "Content-Type", "application/pdf");
    return pdf;
  } finally {
    await browser.close();
  }
});

To turn a component into the source HTML, render it with Vue's renderToString on the server, wrap it in a full document with your CSS, then pass that to page.setContent. Launch the browser once and reuse it across requests rather than per call, because a cold Chromium launch adds about 200 to 400 ms of latency that a warm instance avoids. Puppeteer works the same way if you prefer it; both drive the same Chromium engine, so the PDF output is identical.

Does Playwright work on every Nuxt deployment target?

No. Playwright and Puppeteer need to spawn a Chromium process, which works on the Node-server and container Nitro presets but fails on edge presets. Nitro can deploy the same Nuxt app to Cloudflare Workers, Vercel Edge, Deno Deploy, and Node servers, and the edge runtimes do not allow launching a browser binary.

Nitro presetCan run PlaywrightPDF strategy
node-serverYesPlaywright in a route, or a hosted API
Docker containerYesPlaywright with Chromium system deps installed
Cloudflare WorkersNoBrowser Rendering binding, or a hosted API
Vercel EdgeNoHosted API over fetch
Deno DeployNoHosted API over fetch

If you deploy Nuxt to the edge for cold-start and latency reasons, you cannot bundle Chromium with it. The two options that remain are a browser-rendering service tied to that platform, or an HTTP call to a hosted HTML-to-PDF API from your server route. The API path keeps your code identical across every preset, which is the next section.

How do you generate a PDF in Nuxt without running a browser?

Call a hosted HTML-to-PDF API from a Nuxt server route using the built-in fetch. There is no Chromium binary in your deployment, no process to supervise, and no 300 MB image layer. PDF4.dev renders the HTML on Chromium and returns the PDF, so you keep HTML and CSS as the layout language without operating the browser. This works on every Nitro preset, including the edge runtimes that cannot launch a browser.

export default defineEventHandler(async (event) => {
  const res = await fetch("https://pdf4.dev/api/v1/render", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PDF4_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      template_id: "invoice",
      data: { invoice_number: "INV-001", total: "$1,500.00" },
    }),
  });
 
  setHeader(event, "Content-Type", "application/pdf");
  return Buffer.from(await res.arrayBuffer());
});

For large documents, pass delivery: "url" and get back a signed link instead of the raw bytes, which keeps the route response small. You can test the output on any HTML first, with no code, using the free Html To PdfTry it free tool. The same fetch-based pattern applies in plain Node and Next.js, covered in the Node.js guide and the Next.js guide.

Where the client-side approach breaks in production

Client-side jsPDF plus html2canvas works in a demo and then accumulates problems as documents grow. The failures are predictable, and each one pushes the work toward the server.

Text is not selectable or searchable, because the page is a bitmap. Multi-page documents need manual slicing of the canvas across page boundaries, and page breaks land mid-row. Web fonts that have not finished loading render as fallback fonts in the capture. High-resolution captures (scale: 2 or higher) produce multi-megabyte files. And the full document template plus any data ships to the client, which is a data-exposure problem for invoices or contracts.

If you find yourself adding scale, manual page-splitting, and font-load waits to a client-side exporter, that is the signal to move rendering to the server or a hosted API. The same HTML and CSS produce a vector PDF with none of those workarounds.

Vue and Nuxt PDF generation: which should you choose?

Pick by deployment constraint first, then by fidelity. If you only need a quick browser-side export and selectable text does not matter, jsPDF with html2canvas is the least code. If you run a Node server or container and want full Chromium fidelity you control, run Playwright in a Nitro route. If you deploy to the edge or do not want to operate a browser, call a hosted HTML-to-PDF API over fetch from your server route.

For dynamic invoices, reports, and certificates whose layout changes with the data, the API path removes the operational tail, the Chromium patching, the process supervision, and the image bloat, while keeping HTML and CSS as the template language. The decision is not which engine is better: Playwright and a hosted API both render on Chromium. The decision is whether you want to operate that browser yourself. Create a free PDF4.dev API key to render from a Nuxt route in five lines, or compare the underlying engines in the Playwright vs Puppeteer guide.

Free tools mentioned:

Html To PdfTry it free

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.