HTML to PDF Online Free: A Browser-Local Developer Guide
HTML to PDF online free, browser-local: paste markup, get a PDF, no upload. Compares Puppeteer, wkhtmltopdf, and the html2pdf.js path with honest trade-offs.
You have a Node service that already renders an invoice template to HTML. The HTML is right — line items align, totals reconcile, the logo fits the header. You need a PDF for the email attachment and you have three bad options. Spin up a Puppeteer container in CI and babysit the Chromium binary across deploys. Send the markup to a third-party html2pdf API that logs every request body it sees. Or walk away from automation and let someone hit Print in their browser. None of these are great for a tool you wanted to ship this afternoon.
This guide is about the fourth option: render the HTML to PDF directly in the user's browser, with no upload and no server, using html to pdf online free tools that work the way they advertise. We'll cover why URL-to-PDF cannot honestly ship browser-local (it's a CORS thing — explained plainly below), the HTML-string model that actually works, the three approaches developers compare in 2026, and the trade-offs of each. Every step runs in your tab. Your invoice template, customer data, and contract drafts stay on your laptop.
The dev-loop pain — why HTML to PDF keeps coming up
Most apps end up needing PDF generation eventually. Invoices, statements, receipts, contract templates, packing slips, weekly reports, certificate exports, GDPR data dumps. The market data backs this up: PDF generation searches grow every year and Puppeteer alone has over 4 million weekly npm downloads, the bulk of which are PDF and screenshot use cases.
The pain is in the dev loop. Three flavours of friction show up again and again:
- Heavy server dependency. Puppeteer pulls a 170MB Chromium binary at install time. Your CI image grows by half a gigabyte. Cold starts on serverless deploys hit limits. You spend an afternoon writing a custom Lambda layer just to fit Chromium in 250MB unzipped.
- Trust handoff. SaaS HTML-to-PDF APIs are convenient but every request body — invoice with a customer's home address, paystub with a salary figure, contract with a counterparty — leaves your servers. Their privacy policy promises deletion in 24 hours. You're trusting that promise on every call.
- Brittle render tooling. wkhtmltopdf is fast and widely deployed but its rendering engine is a fork of WebKit from 2012; modern CSS features (grid, flex with gaps, custom properties) render unpredictably. You spend more time debugging CSS regressions than writing the report logic.
Sometimes the right answer is the heavy server with Puppeteer. Sometimes it's the SaaS API with the privacy trade-off documented. And sometimes — for an internal tool, a one-off export, a takehome assessment, an MVP that doesn't yet have a backend — the right answer is to skip the server entirely and do the rendering in the user's tab.
Why URL-to-PDF is impossible browser-local
The obvious feature is URL-to-PDF: paste https://example.com, click Generate, get a PDF of the rendered page. Every search result for “html to pdf online free” promises this. Most of those tools are lying about how they do it, and the rest are running a server. Here is why.
The browser's same-origin policy stops JavaScript running on pdfmavericks.com from reading the response body of a fetch to example.com — unless example.com explicitly opts in via theAccess-Control-Allow-Origin header. This is CORS — Cross-Origin Resource Sharing— the W3C-standard rule that has shaped browser security since 2009. Almost no public website opts in. So a fetch of a third-party URL from your browser either succeeds with an opaque response (no body — useless for HTML-to-PDF) or fails outright.
There is no clever workaround. iframes are blocked by X-Frame-Options and Content-Security-Policy: frame-ancestors on most sites that matter. Browser extensions can bypass CORS but require install permissions a public tool cannot ask for. The only shipping path is a server proxy: the tool's server fetches the URL on your behalf, renders it, returns the PDF. That server sees the URL, your IP, and any cookies you forwarded — and almost always logs them. A “free online URL-to-PDF” service is a server-based service. The browser-local promise is incompatible with the URL-to-PDF feature.
This is the part most marketing pages won't tell you. We'll tell you up front and design around it.
The HTML-string-to-PDF model
The honest browser-local feature is HTML-string to PDF. You bring the markup — already rendered, already styled, already containing whatever data needs to be there. The tool accepts the string in a textarea, renders it in a sandboxed iframe, and converts the iframe contents to a PDF using html2pdf.js, which composes html2canvas (DOM-to-canvas) with jsPDF (canvas-to-PDF).
This model maps cleanly to the developer use cases that drive most real HTML-to-PDF traffic:
- Invoice generation. Your billing service already builds the HTML — line items, totals, customer block, footer. The service runs in your VPC. The HTML never has to leave; you just need the last-mile PDF render. Paste, generate, attach.
- Receipt and statement export. A user requests a receipt for a 2026-04 transaction. Your app renders the receipt template to HTML server-side, returns the HTML, the user's browser converts to PDF, save dialog opens. Zero PDF rendering cost on your servers.
- Contract template. A standard NDA template with party names and effective date filled in. Render the HTML template once on the client, paste into the PDF tool, sign with /sign after.
- Internal report. A weekly metrics report generated from a Notion or Coda export. Copy the rendered HTML, paste it in, get a portable PDF for the leadership thread.
For each of these, the source of truth is the HTML you control. URL-to-PDF would feel cleaner but it isn't the actual job — the actual job is converting markup-you-have to PDF without a server round trip.
Three approaches: Puppeteer, wkhtmltopdf, browser
When developers compare HTML-to-PDF tooling in 2026, three approaches dominate the discussion. Each has a clear best-fit and a clear failure mode.
1. Puppeteer (or Playwright) — server-side, high fidelity
Headless Chromium driven by Node. You boot a browser, navigate to the URL or set the page content from a string, call page.pdf(). The output is a real PDF with selectable text, embedded fonts, and full modern CSS support including @page rules, page-break-inside: avoid, grid, flex with gaps, and custom properties. This is the highest-fidelity option, full stop.
The cost is operational. The Chromium binary alone is about 170MB — bigger than most container images you'd otherwise ship. Cold starts on Lambda or Cloud Run hit memory and timeout limits. You need a Chromium-with-Lambda layer (sparticuz/chromium and similar) and you'll be debugging font rendering on the Lambda runtime when the local container worked fine. For internal tooling on long-lived servers it's the right pick. For a free public tool it doesn't fit.
2. wkhtmltopdf — server-side, fast, dated rendering
A C++ command-line tool that wraps a fork of QtWebKit (the WebKit engine from 2012). Install it, pipe HTML into it, get a PDF. It's blazingly fast, the binary is small, and it's been in production at thousands of companies for over a decade.
The catch is the rendering engine. CSS that works in Chrome and Firefox (grid, flex with gaps, custom properties, modern color functions like oklch()) renders unpredictably or not at all. The project is effectively in maintenance mode — no new releases since 2022, no active engine updates. For templates written in 2012-era CSS it's great. For anything using modern layout features you'll spend more time debugging the renderer than the report logic.
3. Browser-local (html2pdf.js) — privacy + simplicity, rasterized output
The path the PDF Mavericks /html-to-pdf tool ships. html2pdf.js runs entirely in the user's browser, using html2canvas to rasterize the rendered DOM and jsPDF to assemble the PDF. No server round trip, no upload, no install. The HTML stays in the tab.
The trade-off, stated honestly: the output is rasterized. Each page is a PNG embedded in the PDF, which means text is part of the image — not selectable, not searchable, not readable by accessibility tools. This is fine for invoices, receipts, and contract templates where the recipient prints or attaches the file. It's the wrong choice for long-form documents, accessibility-first PDFs, or anything that needs OCR-free text extraction downstream.
The other trade-off is bundle size. html2pdf.js plus html2canvas plus jsPDF totals about 900KB unminified, 300KB gzipped over the wire. We dynamic-import on first interaction so the route load stays under 100KB initial; the libraries fetch when you click Generate. Fine for desktop, acceptable on mobile, slow on metered connections.
Which to pick — three-line decision tree
- Need selectable text or CSS-paged-media features → Puppeteer (server, fidelity premium).
- Need fastest possible server-side render of dated CSS templates → wkhtmltopdf (server, speed premium).
- Need privacy, simplicity, no server, and rasterized output is fine → browser-local html2pdf.js.
How to use /html-to-pdf in 4 steps
- Open the tool. Visit /html-to-pdf. The page loads with a starter invoice template in the editor pane and a live preview iframe on the right. Replace the starter HTML with your own markup — a textarea with line numbers and a monospace font, optimized for paste-and-go.
- Watch the preview render. The iframe re-renders as you type, sandboxed with
allow-same-origin allow-scriptsbut blocked from top navigation, popups, and form submissions. The iframe cannot reach the parent page or any third-party origin — JavaScript inside your HTML can compute totals and format numbers but cannot fetch external APIs at render time. - Pick page size and margin. A4 (210 × 297 mm), Letter (8.5 × 11 in), Legal (8.5 × 14 in), or A5 (148 × 210 mm). Margins at 5, 10, 15 mm presets or a custom millimetre value. Defaults work for most invoices and reports.
- Click Generate PDF. html2pdf.js rasterizes the iframe contents page-by-page at 2× device pixel ratio, jsPDF assembles the canvases into a PDF, and the download starts automatically as
document.pdf. The blob lives in browser memory, served viaURL.createObjectURL(). Open DevTools' Network tab to confirm: zero network requests after the page loads (beyond what your own HTML references — e.g., external image URLs).
Embedding fonts, images, and page breaks
Three rendering details bite developers the first time they use browser-local HTML-to-PDF. State them up front and you'll save the debugging cycle.
Web fonts
@import url(...) from a Google Fonts CDN often fails. html2canvas draws the iframe to a canvas synchronously after the layout settles, but cross-origin font loads can race the draw — by the time the canvas commits, the font may not have replaced the fallback. The reliable pattern is to embed fonts inline as base64 data URIs:
@font-face {
font-family: 'Inter';
src: url(data:font/woff2;base64,d09GMgABAAAAAB...) format('woff2');
}Or skip web fonts entirely and use a system font stack: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif. For invoices and receipts, the system stack is usually fine and the bundle stays small.
External images
<img src="https://example.com/logo.png"> works only when example.com sends Access-Control-Allow-Origin: * or your origin. html2canvas requires crossorigin="anonymous" on the img tag for cross-origin images, and tainted canvases throw on export. Two fixes: (1) host the image on the same origin as your HTML, or (2) embed as base64:
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA..." />
For invoices, embedding the company logo as base64 is the safe default. For one-off documents, copy-paste from your asset pipeline.
Page breaks
CSS page-break-before, page-break-after, and page-break-inside: avoid work — html2pdf.js respects them when slicing the canvas into pages. The newer CSS Paged Media properties (break-before, break-inside) also work in 2026 builds. The shorthand pattern:
.invoice-line-items { page-break-inside: avoid; }
.terms-and-conditions { page-break-before: always; }
@media print {
.no-print { display: none; }
}What does not work: running headers and footers via @page :first, named pages, footnote layout, and the broader CSS-paged-media spec. For those, server-side Chromium-via-Puppeteer is the only option.
The honest trade-offs
Every architecture has costs. The browser-local html2pdf.js path is no exception. Stating them plainly so you can decide:
- Rasterized output, not selectable text. Each page is a PNG inside the PDF. Best fit for invoices, receipts, contract templates. Bad fit for long-form documentation, accessibility-first content, anything that needs text extraction downstream.
- Bundle size on first generate. 300KB gzipped of libraries fetch when you click Generate. Subsequent generates are instant (cached). Fine on desktop, acceptable on mobile, slow on metered connections.
- No URL-to-PDF. CORS makes it impossible browser-local. If your job is “capture this URL”, this tool isn't it — Puppeteer or a headless-browser extension is.
- Memory ceiling around 5MB HTML / 200 pages. html2canvas draws at 2× DPR, which is memory-heavy for long documents. Split into chapters and merge with /merge for very long output.
- External resource fetching is visible. If your HTML embeds
<img src="https://...">, that fetch is visible to your network and DNS. For full air-gap behaviour, embed everything as base64 data URIs.
Where this tool wins: zero server, zero upload, zero signup. The invoice with a customer's home address never leaves the tab. The contract template with the counterparty's name doesn't sit in a vendor S3 bucket. For internal tools, MVPs, takehome assessments, and one-off exports — the cases where you'd otherwise spin up a Puppeteer container or trust a SaaS API — the browser-local path ships the same afternoon and never becomes an ops liability.
For PDF post-processing — adding page numbers to a generated report, stripping metadata before sharing, signing a generated contract, or merging multiple HTML-to-PDF outputs — chain with the related tools below. Everything stays browser-local.
Your HTML never leaves your browser
The HTML you paste, the rendered preview, and the PDF that html2pdf.js generates all live inside this tab. There is no upload step, no temporary copy, no analytics on the markup content. Verify in DevTools' Network tab — generating a PDF triggers no further requests.
Frequently asked questions
Why can't a browser-local tool do URL-to-PDF (point at a URL, get a PDF)?
The same-origin policy and CORS rules stop JavaScript on pdfmavericks.com from reading the rendered HTML of another site. Almost no public website opts in via Access-Control-Allow-Origin, so a fetch of a third-party URL either fails or returns an opaque response with no body. The only working URL-to-PDF path runs on a server (a headless Chromium fetches the page), which means uploading the URL, your IP, and sometimes cookies to that server. A free public tool that ships URL-to-PDF is by definition not browser-local.
Why isn't the text in the output PDF selectable?
html2pdf.js renders by default through html2canvas, which rasterizes each page to a PNG before jsPDF embeds it. The text becomes part of the canvas image, so it cannot be copied, searched, or read by accessibility tools. This is acceptable for invoices and receipts where the recipient prints or attaches the file, but it is the wrong choice for long-form docs or accessibility-first PDFs. For selectable text you need server-side Puppeteer or a different rendering path that walks the DOM into PDF text objects directly.
How do I embed fonts and images so they render reliably?
Inline everything as base64 data URIs. CSS @import url() and @font-face pointing at a third-party CDN often fail because html2canvas draws the page before the cross-origin font request resolves, and CORS blocks most font CDNs without a crossorigin header. The reliable pattern: convert your TTF or WOFF2 to base64 and embed it inline, or stick to a system font stack (system-ui, -apple-system, Segoe UI, Roboto). Same for images — base64 data URIs in img src are bulletproof, while https URLs work only when the host sends Access-Control-Allow-Origin.
Do CSS @page rules and page breaks work?
Partially. html2pdf.js respects page-break-before, page-break-after, and page-break-inside CSS properties, so you can mark elements that should start on a new page. The @page at-rule for setting page size and margins is honored at the html2pdf.js options level, not from CSS — pass jsPDF unit and format in the configuration object. Modern CSS Paged Media features like running headers, named pages, and footnotes do not work; for those you need a real CSS-paged-media renderer like PrinceXML or WeasyPrint, both server-side.
Can I script the workflow or call /html-to-pdf programmatically?
Indirectly. The page is a static HTML+JS bundle, so embedding it in an iframe and driving it via window.postMessage works for trusted internal tooling — your parent app posts the HTML string, the iframe renders and posts back the PDF blob URL. There is no public REST API; the whole point is browser-local execution. For backend automation, run html2pdf.js or its underlying libraries (html2canvas, jsPDF) directly in a Node process — both are npm packages with no service dependency. Or run Puppeteer headless and accept the upload model.
Why is the bundle small enough to ship without lazy loading the libraries?
html2pdf.js is roughly 350KB minified, html2canvas adds another 200KB, and jsPDF is around 350KB — together about 900KB unminified or 300KB gzipped over the wire. We dynamic-import them on first interaction so the initial route load stays under 100KB. Compared to bundling Puppeteer (which includes a 170MB Chromium binary at install time), the browser-side path is cheap. The trade-off is rendering fidelity, not bundle size.
What's the difference between html2pdf.js, jsPDF, and Puppeteer?
jsPDF builds a PDF by drawing primitives (lines, text, images) — you call doc.text(), doc.rect() etc. and it produces a real PDF with selectable text, but it does not understand HTML or CSS. html2canvas takes a DOM element and rasterizes it to a canvas, ignoring the PDF format entirely. html2pdf.js glues the two together: html2canvas rasterizes your DOM to a canvas, then jsPDF embeds that canvas as a page-sized image. The result has zero CSS-rendering surprises but no selectable text. Puppeteer drives a real Chromium and uses its print-to-PDF code path, which produces high-fidelity PDFs with selectable text — at the cost of a 170MB Chromium dependency and a server.
What is the maximum HTML size and rendering time?
Practical limit is about 5MB of HTML or 200 pages of rendered output. Beyond that, html2canvas slows substantially and may exhaust the tab's memory because the canvas is drawn at 2x device pixel ratio for sharpness. A 50-page invoice batch takes a few seconds per page on modern laptop hardware. For very long documents, render in chapters and merge with /merge — the merge step is structural and costs almost nothing on top.