Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the browser’s native print pipeline: add print-only CSS, call window.print() from a user action, and have the user choose Save as PDF in the print dialog. This needs no JavaScript PDF library, but it is interactive—your code cannot receive PDF bytes, silently select a destination, or guarantee the user’s filename and print preferences.

The native, library-free approach

A normal web page already has a PDF-capable output path. CSS defines how the document should look on paper, and JavaScript opens the browser’s print UI:

  1. Write a print stylesheet with @media print.
  2. Set page dimensions and margins with @page where the target browser supports them.
  3. Call window.print() from a button, menu item, or another user-initiated action.
  4. In the dialog, choose Save as PDF (the exact label depends on the browser and operating system).

window.print() opens the dialog for the current document. It does not return a PDF, Blob, ArrayBuffer, or file path; its return value is undefined. The browser may wait for the document to finish loading before opening the dialog, and script execution is paused while the dialog is open.

A complete working example

Save this as an HTML file and open it in a browser. Select Save as PDF after clicking the button.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Invoice</title>
  <style>
    :root {
      font-family: system-ui, sans-serif;
      color: #17202a;
    }

    body {
      max-width: 800px;
      margin: 2rem auto;
      padding: 0 1rem;
      line-height: 1.5;
    }

    .toolbar {
      display: flex;
      gap: .75rem;
      margin-bottom: 2rem;
    }

    .invoice {
      border: 1px solid #d9dee3;
      padding: 2rem;
    }

    .page-break {
      break-after: page;
    }

    @page {
      size: A4;
      margin: 16mm;
    }

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

      body {
        max-width: none;
        margin: 0;
        padding: 0;
      }

      .invoice {
        border: 0;
        padding: 0;
      }

      .page-break {
        break-after: page;
      }
    }
  </style>
</head>
<body>
  <nav class="screen-only">Dashboard · Reports · Settings</nav>
  <div class="toolbar">
    <button id="print-button" type="button">Save as PDF</button>
  </div>

  <main class="invoice">
    <h1>Invoice 1042</h1>
    <p>Issued: 29 September 2026</p>
    <p>Thank you for your business.</p>
    <div class="page-break"></div>
    <h2>Terms</h2>
    <p>Payment is due within 30 days.</p>
  </main>

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

The button remains visible on screen but is removed from the printed output. The second section starts on a new page because of break-after: page.

Control the printed layout with CSS

@media print

Rules inside @media print apply only while the browser is producing print output. Hide navigation, buttons, forms, cookie notices, animations, and other screen-only controls there. You can also reset screen constraints such as a narrow max-width, shadows, sticky positioning, and decorative borders.

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

  .screen-layout {
    display: block;
  }

  a {
    color: inherit;
    text-decoration: none;
  }
}

Use a dedicated class rather than hiding every element by tag when controls are part of the document you need to print. Avoid relying on JavaScript to rewrite the entire DOM just before printing; CSS keeps screen and print concerns separate and is easier to test.

@page for paper size and margins

@page describes the page box used for printing:

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

Common values include A4, letter, and an explicit width and height supported by the browser. You can request landscape with size: A4 landscape. Browser versions and operating systems do not implement every paged-media detail identically, and the print dialog can override these requests. Treat @page as a preferred layout, not a guarantee.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Page breaks

Use modern break properties for deliberate section boundaries:

.cover {
  break-after: page;
}

.chapter {
  break-before: page;
}

.keep-together {
  break-inside: avoid;
}

page-break-after, page-break-before, and page-break-inside are older aliases that can help with older engines. A break can still be moved when the content cannot fit on one physical page, so test long tables, headings near the bottom of a page, and very large elements.

Color, backgrounds, and links

Some browsers omit background colors and images unless the user enables background graphics in the print dialog. print-color-adjust can express that color fidelity matters, but it cannot override a user’s choice:

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

Always provide readable contrast if backgrounds disappear. Decide whether URLs should remain underlined or be shown with generated content; neither choice is automatic, and generated URLs can make a dense document harder to read.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Printing a separate HTML document with a hidden iframe

If the current page is an application shell but the PDF should contain a standalone report, load the report in an off-screen iframe. Wait for its load event before printing, then remove it after printing:

function printDocument(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;

  const removeFrame = () => {
    frame.remove();
  };

  frame.addEventListener('load', () => {
    frame.contentWindow.addEventListener('afterprint', removeFrame, { once: true });
    frame.contentWindow.print();
    // A fallback prevents a retained iframe in browsers that do not fire afterprint.
    setTimeout(removeFrame, 60_000);
  }, { once: true });

  document.body.appendChild(frame);
}

document.querySelector('#print-report').addEventListener('click', () => {
  printDocument('/reports/monthly.html');
});

The report URL must be permitted to load in a frame. A restrictive Content-Security-Policy, X-Frame-Options, authentication flow, or cross-origin policy can prevent access to contentWindow or stop the document from rendering. If the report is generated dynamically, make sure its data and styles are ready before the iframe’s load event; images and fonts that load later can still change pagination.

Preparing dynamic content before printing

Wait for data, images, and fonts

Do not call window.print() while a loading indicator, chart, or asynchronous template is still changing. Resolve your application’s data promise first, then wait for images and fonts where available:

async function waitForAssets() {
  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 });
    });
  }));
}

document.querySelector('#print-button').addEventListener('click', async () => {
  await renderReportData();
  await waitForAssets();
  window.print();
});

Handling an image error as completion prevents one broken image from blocking the print action forever; the print stylesheet should provide an acceptable fallback.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use print lifecycle events sparingly

beforeprint and afterprint can toggle a compact chart, pause an animation, or restore temporary state:

function setPrintMode(enabled) {
  document.documentElement.classList.toggle('is-printing', enabled);
}

window.addEventListener('beforeprint', () => setPrintMode(true));
window.addEventListener('afterprint', () => setPrintMode(false));

Keep the handler synchronous and quick. It is not a reliable place to fetch data or perform a long layout transformation, because the browser may already be preparing pages.

What pure browser printing cannot do

  • No silent file creation: ordinary page JavaScript cannot select the PDF destination or write a file without the user’s interaction.
  • No returned bytes: window.print() does not expose the generated PDF to your code.
  • No guaranteed filename: the browser chooses a default and the user can change it.
  • No fixed preferences: paper source, scaling, headers and footers, background graphics, and destination belong to the print UI and can vary by browser and operating system.
  • No deterministic pagination across every client: fonts, device print settings, browser versions, and available paper sizes affect line wrapping and page breaks.

This makes native printing a good fit for an interactive “download” button in a web page. It is not a fit for unattended jobs, batch exports, server-side archival, or an API that must return identical PDF bytes. Those cases require a rendering service, headless browser automation, or a PDF-generation library, which are outside the no-library constraint.

Troubleshooting checklist

The dialog does not open

  • Call window.print() directly from the click or keyboard handler. Browsers can block calls made much later from unrelated asynchronous callbacks.
  • Check that a JavaScript error earlier in the handler is not aborting execution.
  • Test the browser’s own print command. If it works, inspect your event binding and button type.

Controls or navigation appear in the PDF

Place a specific selector in @media print and use display: none !important when a component’s inline style or higher-specificity rule wins. Check both the visible element and any portal or fixed-position copy mounted elsewhere in the document.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Content is cut off or shrunk

  • Remove fixed heights and overflow: hidden from print containers.
  • Reset screen-only max widths and transforms.
  • Check the dialog’s scale setting and margins; browser defaults can reduce the printable area.
  • Give wide tables a print layout or allow horizontal content to wrap.

Page breaks are ignored

Apply the break to a block-level element, ensure no ancestor uses a layout that conflicts with fragmentation, and try both break-after and its legacy page-break-after alias for older browsers. A break cannot force an oversized element to fit on one page.

Images, fonts, or colors are missing

Wait for assets before printing, verify that URLs are reachable without an authenticated request that the browser cannot reuse, and tell users to enable background graphics when the design depends on them. Provide a system-font fallback so text remains legible if a web font fails.

The iframe prints a blank page

Wait for the iframe’s load event, confirm the response is HTML rather than a redirect to a login page, and check framing headers and cross-origin restrictions. Remove the iframe in afterprint plus a timeout fallback so failed print flows do not accumulate hidden documents.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability, and security considerations

Printing is usually inexpensive because layout happens in the user’s browser, but very large DOM trees, high-resolution images, and complex charts increase layout time and memory use. Render only the report content, avoid unnecessary animations, and use print-specific simplified charts when a full interactive visualization adds no value.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Printing does not bypass access controls. The browser can print whatever the current user can view, so protect report routes and avoid placing secrets in hidden elements: display: none content can still be included accidentally if your selectors are too broad. Sanitize user-generated HTML before inserting it into a printable document, just as you would for the screen version.

For repeatable output, test the exact browser and operating-system combinations your users have. Compare page count, margins, fonts, color behavior, and table breaks; do not assume that a result in one print preview is identical elsewhere.

Or skip the browser setup

If you need a URL turned into a PDF without asking a person to open a print dialog, ScreenshotNeo provides a website screenshot API that can return PDF as well as PNG, JPEG, or WebP. It handles page rendering on the service side and exposes PDF settings such as paper size, margins, landscape mode, and page ranges. See the ScreenshotNeo documentation for the current request options.

A basic request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same endpoint can be used from JavaScript or Python:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

For a PDF request, select the PDF output and its paper settings in the API options. ScreenshotNeo removes cookie-consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status with X-Page-Verdict and X-Billed headers. Its MCP server includes take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan, and yearly billing provides two months free. Create a free ScreenshotNeo account to try it without adding a card.

Choosing the right method

Requirement Native print Rendering service or automation
A user can confirm settings Excellent; the browser dialog supplies destination and preferences. Usually unnecessary.
Return PDF bytes to application code Not available from ordinary page JavaScript. Supported by a service or controlled browser workflow.
Exact filename and silent operation Not guaranteed. Possible when the chosen service or automation controls output.
Batch URLs or scheduled jobs Unsuitable. Designed for this use case.
No external dependency Yes. No; it introduces a service, browser runtime, or library.

Frequently Asked Questions

Does window.print() work from a keyboard shortcut or only a button?

It can be called from any user-initiated action, including a keyboard handler, provided the browser allows the print request. A visible button is the clearest accessible fallback.

Can I print only one div without opening another page?

Yes. Put the rest of the page in a print-hidden wrapper, or use print CSS to hide everything except the target element. The browser still prints a document; it does not serialize an isolated element directly into PDF bytes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does the PDF have a different number of pages on two computers?

Pagination depends on browser and operating-system print settings, available fonts, paper size, margins, scaling, and background preferences. Native printing is therefore not a byte-for-byte rendering contract.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.