ScreenshotNeo

BlogHow-to

How to Capture and Print a Complete DOM Element in Angular

Capture one complete Angular element and print it reliably with print CSS, an isolated iframe, or ScreenshotNeo.

By the ScreenshotNeo team1 October 20268 min read

Short answer: Angular can give you the component host through ElementRef, but window.print() prints the current document and does not accept an element. To print one complete element, wait until Angular has rendered it, then either hide everything else with @media print or copy the element into a dedicated document or iframe. Use print CSS first; use an iframe when you need stronger isolation.

This guide covers a complete Angular implementation, including host and descendant selection, render timing, images and fonts, page breaks, encapsulation, SSR, iframe printing, troubleshooting, and an API alternative.

1. Understand the Angular and browser constraints

An Angular component’s selector-matching element is its host, and the component template is rendered inside that element. Angular’s ElementRef.nativeElement exposes the host element. Angular does not guarantee that a component DOM is fully rendered outside a render callback, so DOM-dependent setup belongs in afterNextRender or, when genuinely needed, afterEveryRender. See Angular’s DOM API guidance.

The browser API is document-scoped: window.print() opens the print dialog for the current document. It has no element parameter. The target-only behavior must therefore come from CSS or a separate document. See MDN’s Window.print() reference.

Keeping the current document preserves the Angular component’s styles, loaded fonts, images, and application state. Add a stable ID or class to the element to print, hide unrelated content in print media, and call window.print() from a user action.

Angular component

import { Component, ElementRef, inject, afterNextRender } from '@angular/core';

@Component({
  selector: 'app-invoice',
  template: `
    <article class="invoice" id="invoice-to-print">
      <h1>Invoice #1042</h1>
      <p>Customer: Ada Lovelace</p>
      <table>
        <tr><th>Item</th><th>Amount</th></tr>
        <tr><td>Implementation</td><td>$1,200</td></tr>
      </table>
      <button type="button" (click)="print()">Print invoice</button>
    </article>
  `
})
export class InvoiceComponent {
  private readonly host = inject(ElementRef<HTMLElement>);

  constructor() {
    afterNextRender(() => {
      // The host and its descendants are safe to inspect after rendering.
      const target = this.host.nativeElement.querySelector('#invoice-to-print');
      if (!target) throw new Error('Print target was not rendered');
    });
  }

  print(): void {
    window.print();
  }
}

If the component host itself is the print unit, put the ID on the host in the parent template and use this.host.nativeElement directly. If the target is a descendant, query it after render as shown above. Prefer Angular bindings and template control flow for normal UI changes; use DOM APIs only for this boundary with the browser.

@media print {
  /* Hide the application shell and controls. */
  body * {
    visibility: hidden;
  }

  /* Reveal the target and every descendant. */
  #invoice-to-print,
  #invoice-to-print * {
    visibility: visible;
  }

  /* Move the target to the printable page origin. */
  #invoice-to-print {
    position: absolute;
    inset: 0;
    width: 100%;
    max-width: none;
  }

  /* Avoid printing controls inside the target. */
  #invoice-to-print button,
  #invoice-to-print .no-print {
    display: none;
  }

  /* Keep useful blocks together where possible. */
  #invoice-to-print table,
  #invoice-to-print img,
  #invoice-to-print .section {
    break-inside: avoid;
  }

  /* Repeat table headings on paginated output. */
  #invoice-to-print thead {
    display: table-header-group;
  }
}

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

The visibility pattern keeps the target in layout while hiding other content. For unrelated UI, display: none can be simpler, but do not remove an ancestor that supplies the target’s required layout. Fixed heights, overflow: hidden, transforms, and positioned ancestors can clip long output; override them in print styles.

Wait for data, images, and fonts before opening the dialog. A user click is preferable because browsers may restrict print dialogs opened without an intentional interaction.

async printWhenReady(): Promise<void> {
  await this.loadInvoiceData();

  const target = this.host.nativeElement.querySelector('#invoice-to-print');
  if (!target) throw new Error('Missing print target');

  const images = Array.from(target.querySelectorAll('img'));
  await Promise.all(images.map((image) => {
    if (image.complete) return Promise.resolve();
    return new Promise<void>((resolve) => {
      image.addEventListener('load', () => resolve(), { once: true });
      image.addEventListener('error', () => resolve(), { once: true });
    });
  }));

  if ('fonts' in document) await document.fonts.ready;
  window.print();
}

3. Choose the exact element to capture

Target How to select it When to use it
Component host inject(ElementRef<HTMLElement>).nativeElement The whole component is the print unit.
Descendant host.nativeElement.querySelector('.receipt') The component contains controls or multiple sections.
Multiple regions Give each region its own print class or render a print-specific view. Each section needs separate pagination or actions.

Use a stable class or ID rather than a generated Angular attribute. A selector must still match when emulated encapsulation is enabled. With Shadow DOM encapsulation, document-level selectors cannot cross the shadow boundary; place the print rules inside the shadow root or use a dedicated print document.

4. Use an isolated iframe when CSS isolation matters

An iframe lets you create a print-only document containing the target markup. It is useful when the application has complex global styles, overlays, fixed containers, or several unrelated print rules. The copied document must also receive the styles, fonts, images, and other assets it needs. MDN documents the hidden-iframe pattern for printing another document: Printing.

export async function printElementInIframe(element: HTMLElement): Promise<void> {
  const iframe = document.createElement('iframe');
  iframe.setAttribute('aria-hidden', 'true');
  iframe.style.position = 'fixed';
  iframe.style.width = '0';
  iframe.style.height = '0';
  iframe.style.border = '0';
  iframe.style.right = '0';
  iframe.style.bottom = '0';
  document.body.appendChild(iframe);

  try {
    const printDocument = iframe.contentDocument;
    const printWindow = iframe.contentWindow;
    if (!printDocument || !printWindow) throw new Error('Could not create print document');

    const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style'))
      .map((node) => node.outerHTML)
      .join('\n');

    printDocument.open();
    printDocument.write(`<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    ${styles}
    <style>
      @page { margin: 16mm; }
      body { margin: 0; }
    </style>
  </head>
  <body>${element.outerHTML}</body>
</html>`);
    printDocument.close();

    await new Promise<void>((resolve) => {
      if (printDocument.readyState === 'complete') return resolve();
      printWindow.addEventListener('load', () => resolve(), { once: true });
    });

    const images = Array.from(printDocument.images);
    await Promise.all(images.map((image) => image.complete
      ? Promise.resolve()
      : new Promise<void>((resolve) => {
          image.addEventListener('load', () => resolve(), { once: true });
          image.addEventListener('error', () => resolve(), { once: true });
        })));

    printWindow.focus();
    printWindow.print();
  } finally {
    // Give the print dialog time to take ownership of the document.
    setTimeout(() => iframe.remove(), 1000);
  }
}

Copying outerHTML copies markup, not Angular behavior. Interactive bindings, event listeners, and component state do not come along. For a print view, render the required values as static markup and include absolute or otherwise reachable asset URLs.

5. Angular rendering, SSR, and encapsulation

  • Render timing: use afterNextRender for one-time setup. Use afterEveryRender only when repeated DOM work is required.
  • SSR and prerendering: render callbacks do not run during server execution. Keep window, document, and window.print() behind a browser-only interaction.
  • Emulated encapsulation: component styles are rewritten to match Angular’s generated attributes. Put global @media print rules in a global stylesheet when they must affect the whole page.
  • Shadow DOM: document-level print selectors do not enter a shadow root. Add print CSS inside the shadow root or use an isolated print view.

6. Page size, pagination, and complete content

Use @page for paper size, orientation, and margins. Use break-before, break-after, and break-inside for page flow, but validate long content in the browsers, operating systems, printers, and PDF drivers you support. Browser and printer pagination is not identical everywhere.

@media print {
  .page-break-before { break-before: page; }
  .avoid-break { break-inside: avoid; }
  .print-only { display: block; }
}

@media screen {
  .print-only { display: none; }
}

@page {
  size: Letter landscape;
  margin: 12mm 10mm;
}

Do not rely on a fixed pixel height for the target. Remove clipping, allow width to become printable width, and check that images have intrinsic dimensions or reserved space so late loading does not shift pagination.

7. Troubleshooting

Symptom Likely cause Fix
Nothing prints The selector does not match or the target is still hidden. Inspect the rendered DOM after afterNextRender; verify the ID/class and print rules.
The whole page prints The hide/reveal selectors are too broad or overridden by more specific CSS. Inspect computed print styles and increase specificity only for the target.
Target is blank Data, images, or fonts have not finished loading. Await data, document.fonts.ready, and image load/error events before printing.
Content is cut off An ancestor has fixed height or overflow: hidden. Reset height and overflow in @media print; remove transforms and clipping containers.
Styles disappear in iframe Only markup was copied. Copy stylesheet links and inline styles, and ensure fonts and images are reachable.
Shadow DOM styles do not apply Document selectors cannot cross the shadow boundary. Put print rules in the shadow root or render a dedicated print document.
Print fails during SSR window or document was accessed on the server. Call printing only from a browser event and guard browser-only code.
Dialog does not open The call was not triggered by a user gesture or another dialog is open. Call from a button click and avoid repeated automatic calls.

8. Performance, reliability, and cost considerations

  • Print CSS avoids copying markup and usually has the least setup work.
  • An iframe duplicates markup and style loading, so it can take longer for large documents and many images.
  • Waiting for all images improves completeness but can delay printing when a remote asset is slow. Resolve failed image loads and show a useful fallback.
  • Large tables and long pages depend on browser pagination. Use repeated table headers and break rules, then validate representative documents.
  • window.print() blocks while the print dialog is open. Do not treat code after the call as a reliable completion callback for printer output.

9. Or skip the browser setup

If you need a PNG, JPEG, WebP, or PDF of a rendered URL rather than an interactive local print dialog, ScreenshotNeo provides a GET-based screenshot API and an MCP server. Cookie banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. An MCP server lets AI agents take screenshots, and the free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

See the ScreenshotNeo API documentation for options such as full-page capture, CSS selectors, custom CSS and JavaScript, waits, device presets, PDF output, and signed webhooks.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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}`);

Responses include X-Page-Verdict and X-Billed headers so you can see whether a response was a clean, billable capture. Create a free ScreenshotNeo account with 1,000 screenshots each month and no card.

10. FAQ

Can I pass an element directly to window.print()?

No. The method prints the current document. Use print CSS or an iframe.

Should I use ElementRef or document.querySelector?

Use an injected ElementRef to scope a descendant query to the component. This reduces accidental matches elsewhere on the page.

Does iframe printing preserve Angular event handlers?

No. Copying HTML copies the rendered markup only. Print views should contain the values and assets needed for output, not interactive behavior.

Why does the PDF look different from the screen?

Print media rules, paper dimensions, browser pagination, font availability, and printer or PDF-driver settings can all change the result. Test the deployment combinations you support.