ScreenshotNeo

BlogHTML to image & PDF

How to Convert HTML to PDF in JavaScript Without a Library

Use window.print(), @media print, and @page to turn HTML into a PDF from JavaScript without jsPDF or another library.

By the ScreenshotNeo team1 October 20267 min read

Use the browser’s native print pipeline. Style the document for print with @media print, set page geometry with @page, and call window.print() from a user action. The browser opens its print dialog, where the user chooses Save as PDF. Pure browser JavaScript cannot silently return PDF bytes, select the destination, or guarantee a filename.

Minimal working example

Save this as an HTML file and open it in a browser:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Invoice</title>
  <style>
    @page {
      size: A4;
      margin: 16mm;
    }

    body {
      font: 16px/1.5 system-ui, sans-serif;
      color: #111;
    }

    @media print {
      #print-button,
      nav,
      .screen-only {
        display: none !important;
      }

      .page-break {
        break-after: page;
      }
    }
  </style>
</head>
<body>
  <nav>Dashboard &middot; Settings</nav>
  <main>
    <h1>Invoice 1042</h1>
    <p>This content appears in the PDF.</p>
    <button id="print-button" type="button">Save as PDF</button>
  </main>

  <script>
    document.querySelector('#print-button').addEventListener('click', () => {
      window.print();
    });
  </script>
</body>
</html>

window.print() opens the print dialog for the current document. It waits for the document to finish loading when necessary, blocks while the dialog is open, and returns undefined. See the MDN print() reference.

What “without a library” can and cannot mean

Requirement Native browser printing
Trigger a PDF save flow Yes, with window.print()
Hide controls in printed output Yes, with @media print
Set paper size and margins Yes, where the browser supports the @page rule
Return a Blob or PDF bytes to JavaScript No
Choose a filename or destination silently No; the print UI or automation controls these
Generate PDFs unattended on a server No; use a headless browser, rendering service, or PDF library

Build a print-specific layout

Hide screen-only elements

@media print {
  nav,
  button,
  form,
  .screen-only,
  .chat-widget,
  .animation {
    display: none !important;
  }
}

Keep the content you want exported in the document and remove navigation, editing controls, forms, animations, and other screen-only UI when printing.

Set paper size, orientation, and margins

@page {
  size: A4 portrait;
  margin: 16mm 14mm 18mm;
}

/* US paper */
@page.letter {
  size: letter;
}

/* Landscape output */
@page.landscape {
  size: A4 landscape;
}

Use A4, letter, or an explicit size supported by the target browser. A named page rule can be selected with the CSS page property:

.wide-table {
  page: landscape;
}

Browser support for parts of @page varies, especially on older devices. Always check the browser and operating system your users rely on.

Control page breaks

.chapter {
  break-after: page;
}

.avoid-split {
  break-inside: avoid;
}

/* Legacy spelling for older engines */
.legacy-break {
  page-break-after: always;
}

Use break-after: page between deliberate sections and break-inside: avoid for cards, table rows, signatures, or other blocks that should stay together. Very large blocks can still be split when they cannot fit on one page.

@media print {
  .brand-panel {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
}

Color output also depends on the browser’s “background graphics” setting. Users can override print preferences, so do not make essential information depend only on a background color.

To print another page without navigating away from the current one, create an iframe, wait for its load event, print its window, and remove it afterward:

function printUrl(url) {
  const frame = document.createElement('iframe');
  frame.style.position = 'fixed';
  frame.style.right = '0';
  frame.style.bottom = '0';
  frame.style.width = '0';
  frame.style.height = '0';
  frame.style.border = '0';
  frame.src = url;

  frame.addEventListener('load', () => {
    const printWindow = frame.contentWindow;
    const cleanup = () => {
      window.removeEventListener('afterprint', cleanup);
      frame.remove();
    };

    window.addEventListener('afterprint', cleanup, { once: true });
    printWindow.focus();
    printWindow.print();

    // Fallback for browsers that do not fire afterprint reliably.
    setTimeout(cleanup, 60_000);
  }, { once: true });

  document.body.appendChild(frame);
}

document.querySelector('#print-other-page').addEventListener('click', () => {
  printUrl('/report.html');
});

The iframe page must be permitted to load and print in the browser’s security model. Cross-origin pages can restrict script access, and authentication or CSP rules may prevent the page from rendering as expected. MDN documents print media behavior and the beforeprint/afterprint lifecycle events.

Wait for dynamic content before printing

Call print only after data, images, and fonts needed for the document are ready:

async function printWhenReady() {
  if (document.fonts?.ready) {
    await document.fonts.ready;
  }

  const images = [...document.images];
  await Promise.all(images.map(image => {
    if (image.complete) return Promise.resolve();
    return new Promise(resolve => {
      image.addEventListener('load', resolve, { once: true });
      image.addEventListener('error', resolve, { once: true });
    });
  }));

  window.print();
}

document.querySelector('#print-button').addEventListener('click', printWhenReady);

This avoids printing before late-loading assets arrive. It does not bypass a blocked request, a failed image, or a page that requires server-side data.

Common edge cases

  • Printing a div: CSS prints the document, not an arbitrary DOM node as a standalone PDF. Put the target content in a print layout, hide everything else, or open a dedicated print document.
  • Fixed headers and footers: A fixed element can overlap content across pages. Prefer normal flow and margins; test repeating headers in each target browser.
  • Long tables: Use table headers with <thead>, avoid splitting critical rows, and test wide tables in landscape mode.
  • Canvas charts: Ensure the canvas is drawn before printing. If a chart is animated, pause it during beforeprint.
  • Videos and animations: Hide them or replace them with a poster image in print CSS.
  • Fonts: Wait for document.fonts.ready; otherwise fallback fonts can change line wrapping and pagination.
  • Links: Browsers may add URLs to printed output depending on print-dialog settings. JavaScript cannot force that preference.
  • Authentication: The print view must be accessible in the same browser session. A server-generated print route is often simpler for private reports.

Troubleshooting

Symptom Likely cause Fix
Nothing happens Print is not called from a user gesture, or a popup blocker intervenes Call it directly from a click or keyboard event and check the console
Buttons appear in the PDF No print rule hides them Add them to an @media print selector with display: none !important
Content is cut off Fixed heights, overflow rules, or unsuitable margins Remove restrictive heights and review overflow, @page, and print preview
Unexpected page breaks Different font metrics or break rules Wait for fonts, use break-inside: avoid where appropriate, and test the target browser
Backgrounds are missing Background graphics disabled in print preferences Use strong foreground contrast and test print-color-adjust; users may still override it
Iframe prints a blank page The iframe has not loaded, is blocked, or is cross-origin Wait for load, verify the URL and permissions, and use a same-origin print route when possible
Images are missing Images are still loading, failed, or require credentials Wait for image load events and verify request access before calling print
PDF destination or filename cannot be set Native printing intentionally delegates those controls to the browser Use browser automation or a server-side PDF tool for deterministic output

Performance, reliability, and cost

  • Performance: Printing is usually limited by layout, fonts, images, and the print preview itself. Reduce unnecessary DOM, wait only for assets required in the PDF, and avoid expensive animations.
  • Reliability: Output depends on browser version, operating system, print settings, installed fonts, and user interaction. Treat print preview as the source of truth for client-side workflows.
  • Cost: The browser API has no service fee, but it requires a person (or browser automation) to complete the print flow. Batch, unattended, or server-side generation needs an automation or rendering system.
  • Security: Do not place secrets in printable HTML. A print view can expose everything rendered into the page, and iframe printing must respect same-origin and content-security policies.

Or skip the browser setup

If you need a server response instead of an interactive print dialog, ScreenshotNeo can capture a URL as a PDF with one request. Its PDF options include paper size, margins, landscape mode, and page ranges. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -d format=pdf \
  -o page.pdf
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://stripe.com",
        "format": "pdf",
    },
    timeout=90,
)
r.raise_for_status()
open("page.pdf", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
  format: 'pdf'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = await res.arrayBuffer();
await Bun.write('page.pdf', bytes); // or write bytes with your Node.js filesystem code

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and whether it was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can JavaScript save a PDF silently?

No. In a normal web page, window.print() opens the browser UI. Selecting Save as PDF, the filename, destination, and several print preferences remain under browser or automation control.

Can I convert only one element?

Not directly with the native API. Create a print-specific document or hide all other content with print CSS. For deterministic element capture, use a rendering service or automation.

Does this work without jsPDF?

Yes. The example uses only HTML, CSS, and the built-in window.print() method.

Why does pagination differ between browsers?

Fonts, layout engines, supported @page features, margins, and user print preferences differ. Validate the exact browser and operating system your users will use.

When should I choose a server-side PDF workflow?

Choose one when you need unattended generation, stable files, batch jobs, controlled filenames, webhooks, or output independent of a user’s print settings.