<!DOCTYPE html>
<html lang="en">
<head><script type="importmap">{"imports": {"@zumer/snapdom": "https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.mjs", "@zumer/snapdom/plugins": "https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.mjs"}}</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Capture an Angular Component as an Image — SnapDOM</title>
<meta name="description" content="Capture an Angular component as PNG or SVG with SnapDOM using viewChild and the correct browser lifecycle." />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://snapdom.dev/guides/angular/" />
<link rel="help" type="text/plain" href="https://snapdom.dev/llms.txt" title="LLM summary">
<link rel="help" type="text/plain" href="https://snapdom.dev/llms-full.txt" title="LLM full reference">
<meta property="og:type" content="article" />
<meta property="og:title" content="Capture an Angular Component as an Image" />
<meta property="og:description" content="Capture an Angular component as PNG or SVG with SnapDOM using viewChild and the correct browser lifecycle." />
<meta property="og:url" content="https://snapdom.dev/guides/angular/" />
<meta property="og:image" content="https://snapdom.dev/assets/og-card.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/png" href="../../assets/favicon/favicon-96x96.png" sizes="96x96">
<link rel="stylesheet" href="../../shared.css"><link rel="stylesheet" href="../../page.css">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Capture an Angular component","description":"Capture an Angular component as PNG or SVG with SnapDOM using viewChild and the correct browser lifecycle.","url":"https://snapdom.dev/guides/angular/","datePublished":"2026-07-16","dateModified":"2026-10-15","author":{"@type":"Organization","name":"Zumerlab"},"dependencies":"SnapDOM (@zumer/snapdom)"}</script>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Why does the PNG show the previous signal value?","acceptedAnswer":{"@type":"Answer","text":"The capture ran before Angular applied the signal change to the DOM. Register afterNextRender with the component injector, then capture from its callback."}},{"@type":"Question","name":"Can I capture an Angular component on the server?","acceptedAnswer":{"@type":"Answer","text":"This recipe captures a view in the browser after it becomes interactive. Server-side screenshot jobs need a browser automation tool to load and render the route first."}}]}</script>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"SnapDOM","item":"https://snapdom.dev/"},{"@type":"ListItem","position":2,"name":"Guides","item":"https://snapdom.dev/guides/"},{"@type":"ListItem","position":3,"name":"Capture an Angular component","item":"https://snapdom.dev/guides/angular/"}]}</script>
<link rel="stylesheet" href="../../navigation.css"><link rel="stylesheet" href="../../panes.css">
<script type="module" src="../../panes.js"></script>
<link rel="stylesheet" href="../../code.css"><script defer data-manual src="../../assets/prism/prism.min.js"></script><script defer src="../../code-blocks.js"></script></head>
<body>
<script defer src="../../site.js"></script>
<header class="site-head"><div class="site-head-inner"><a class="site-shell-brand" href="../../" aria-label="SnapDOM home"><span class="site-shell-mark"></span><span>SnapDOM</span></a><nav class="site-nav" aria-label="Main navigation"><div class="site-nav-group is-current"><button type="button" class="site-nav-button" aria-expanded="false">Learn</button><div class="site-nav-panel"><a class="site-nav-item sec-capture" href="../../docs/"><span class="sec-dot"></span><span class="site-nav-item-label">Docs</span><span class="site-nav-item-desc">API, options and quick start</span></a><a class="site-nav-item sec-start" href="../../how-to/" aria-current="page"><span class="sec-dot"></span><span class="site-nav-item-label">How-to</span><span class="site-nav-item-desc">Recipes and framework guides</span></a><a class="site-nav-item sec-ecosystem" href="../../compare/"><span class="sec-dot"></span><span class="site-nav-item-label">Compare</span><span class="site-nav-item-desc">SnapDOM vs html2canvas and others</span></a></div></div><div class="site-nav-group"><button type="button" class="site-nav-button" aria-expanded="false">Explore</button><div class="site-nav-panel"><a class="site-nav-item sec-labs" href="../../showcase/"><span class="sec-dot"></span><span class="site-nav-item-label">Showcase</span><span class="site-nav-item-desc">Live demos and labs</span></a><a class="site-nav-item sec-ecosystem" href="../../showcase/#plugins"><span class="sec-dot"></span><span class="site-nav-item-label">Plugins</span><span class="site-nav-item-desc">PDF, HTML, vector, GIF, video, agent context</span></a><a class="site-nav-item sec-record" href="../../blog/"><span class="sec-dot"></span><span class="site-nav-item-label">Blog</span><span class="site-nav-item-desc">Notes from the team</span></a></div></div><div class="site-nav-group"><button type="button" class="site-nav-button" aria-expanded="false">Community</button><div class="site-nav-panel"><a class="site-nav-item sec-ecosystem" href="../../ecosystem/"><span class="sec-dot"></span><span class="site-nav-item-label">Ecosystem</span><span class="site-nav-item-desc">SnapDIFF, SnapEye, SnapSurf</span></a><a class="site-nav-item sec-context" href="../../made-with/"><span class="sec-dot"></span><span class="site-nav-item-label">Made with</span><span class="site-nav-item-desc">Projects using SnapDOM</span></a></div></div></nav><button type="button" class="site-menu-button" aria-expanded="false">Menu</button><a class="site-shell-github" aria-label="SnapDOM on GitHub" href="https://github.com/zumerlab/snapdom" target="_blank" rel="noopener"><span>GitHub</span><span class="site-shell-stars">★ <span data-star-count>8K</span></span></a></div></header>
<main id="main" class="pane no-demo" data-pane>
<aside class="pane-side"><label class="pane-filter"><span aria-hidden="true">⌕</span><input type="search" placeholder="Filter recipes and guides" aria-label="Filter recipes and guides"></label><nav class="pane-nav" aria-label="How-to"><div class="sec-start"><button type="button" class="pane-nav-head" aria-expanded="false"><span class="sec-dot"></span>Overview</button><div class="pane-nav-items"><a href="../../how-to/">How to capture the DOM</a></div></div><div class="sec-start"><button type="button" class="pane-nav-head" aria-expanded="false"><span class="sec-dot"></span>Basics</button><div class="pane-nav-items"><a href="../../how-to/screenshot-a-div/">Screenshot a div</a><a href="../../how-to/html-to-png/">Convert HTML to PNG</a><a href="../../how-to/html-to-svg/">Convert HTML to SVG</a><a href="../../how-to/html-to-canvas/">Render HTML to Canvas</a><a href="../../how-to/high-resolution-element/">Capture in high resolution</a></div></div><div class="sec-capture"><button type="button" class="pane-nav-head" aria-expanded="false"><span class="sec-dot"></span>Real content</button><div class="pane-nav-items"><a href="../../how-to/export-dashboard-as-image/">Export a dashboard</a><a href="../../how-to/export-react-component-as-image/">Export a React component</a><a href="../../how-to/export-invoice-as-image/">Export an invoice</a><a href="../../how-to/generate-social-card-from-html/">Generate a social card</a><a href="../../how-to/capture-before-pdf/">Capture before a PDF</a><a href="../../how-to/export-chart-as-svg/">Export a chart as SVG</a><a href="../../how-to/save-chart-js-as-png/">Save a Chart.js chart</a></div></div><div class="sec-ecosystem"><button type="button" class="pane-nav-head" aria-expanded="false"><span class="sec-dot"></span>Special cases</button><div class="pane-nav-items"><a href="../../how-to/capture-element-without-puppeteer/">Capture without Puppeteer</a><a href="../../how-to/capture-shadow-dom/">Capture Shadow DOM</a><a href="../../how-to/capture-full-page/">Capture a full page</a><a href="../../how-to/capture-iframe/">Capture an iframe</a><a href="../../how-to/visual-regression-screenshot/">Visual regression</a></div></div><div class="sec-options"><button type="button" class="pane-nav-head" aria-expanded="true"><span class="sec-dot"></span>Frameworks</button><div class="pane-nav-items"><a href="../react/">React</a><a href="../vue/">Vue</a><a href="../nextjs/">Next.js</a><a href="../svelte/">Svelte</a><a href="./" aria-current="page">Angular</a><a href="../lit/">Lit</a></div></div></nav></aside>
<article class="pane-read sec-options" data-title="Angular">
<header class="doc-head"><div class="doc-head-bar"><nav class="doc-crumbs" aria-label="Breadcrumb"><span>How-to</span><span aria-hidden="true">/</span><span>Frameworks</span><span aria-hidden="true">/</span><span>Angular</span></nav><span class="doc-meta">Framework Guide · Angular 19+</span></div><h1>Capture an <span class="accent">Angular component</span></h1><p class="doc-lead">Download a signal-driven Angular view after its next render, with a typed viewChild reference and asset readiness checks.</p></header>
<div class="pane-content"><div class="tldr"><div class="tldr-label">TL;DR</div><p>Use <code>viewChild.required</code> to select the rendered card and <code>afterNextRender</code> to capture after signal updates reach the DOM. Keep the export button outside the captured element.</p></div>
<section class="prose"><h2 id="install-and-use-snapdom-in-angular">Install and use SnapDOM in Angular</h2><p>Install the capture library in your existing Angular project:</p><pre class="code-block">npm install @zumer/snapdom</pre><p>Add this standalone component to your Angular view.</p><pre class="code-block">import { Component, ElementRef, Injector, afterNextRender,
inject, signal, viewChild } from '@angular/core'
import { snapdom } from '@zumer/snapdom'
@Component({
selector: 'app-export-card',
standalone: true,
template: `
<input aria-label="Card title" [value]="title()"
(input)="title.set($any($event.target).value)" />
<article #card><h2>{{ title() }}</h2><p>Ready to share.</p></article>
<button (click)="save()" [disabled]="saving()">Download PNG</button>
<p role="status">{{ error() }}</p>
`,
})
export class ExportCardComponent {
card = viewChild.required<ElementRef<HTMLElement>>('card')
title = signal('Monthly report')
saving = signal(false)
error = signal('')
private injector = inject(Injector)
save() {
this.saving.set(true)
this.error.set('')
afterNextRender(async () => {
try {
const el = this.card().nativeElement
await document.fonts.ready
await Promise.all(Array.from(el.querySelectorAll('img'), img => img.decode()))
await snapdom.download(el, { format: 'png', filename: 'angular-card' })
} catch (error) {
this.error.set(error instanceof Error ? error.message : 'Capture failed')
} finally {
this.saving.set(false)
}
}, { injector: this.injector })
}
}</pre></section>
<section class="prose"><h2 id="lifecycle-and-fidelity">Wait for Angular’s next render</h2><p>The click handler may change signals, fetch data or reveal an <code>@if</code> block. Reading the DOM immediately after that change can capture the previous view. <a href="https://angular.dev/api/core/afterNextRender">Angular’s afterNextRender callback</a> runs after the next application render; pass the component’s injector when registering it from an event handler.</p><p>The example’s <code>saving</code> signal schedules that render. An optional <code>viewChild</code> can be undefined when its element is absent; use a required query only for a card that is always rendered. If the card is conditional, wait until it exists before enabling export.</p></section>
<section class="prose"><h2 id="assets">Wait for data, images and fonts</h2><p>Finish the application’s data request before scheduling capture. <code>afterNextRender</code> waits for Angular’s DOM work, not network responses. The example then waits for <code>document.fonts.ready</code> and decodes images inside the card; a broken image reaches the status message instead of silently producing an incomplete report.</p><p>For an exportable chart, wait for its own animation or ready event too. When the output is destined for an upload instead of a download, follow the <a href="../../how-to/html-to-png/#upload">PNG Blob and FormData pattern</a>.</p></section>
<section class="prose"><h2 id="browser-only-boundary">Angular SSR and hydration</h2><p>Keep the export attached to a browser click. Render callbacks do not run during server-side rendering, and a server cannot provide the live layout that SnapDOM captures. For a hydrated application, wait until the component is interactive and its data has settled; a render callback alone is not a blanket guarantee that every child has completed hydration.</p></section>
<section class="prose faq"><h2 id="faq">Frequently asked questions</h2><details><summary>Why does the PNG show the previous signal value?</summary><p>The capture ran before Angular applied the signal change to the DOM. Register afterNextRender with the component injector, then capture from its callback.</p></details><details><summary>Can I capture an Angular component on the server?</summary><p>This recipe captures a view in the browser after it becomes interactive. Server-side screenshot jobs need a browser automation tool to load and render the route first.</p></details></section>
<section class="related"><div class="section-label">Related</div><div class="related-grid"><a class="related-card" href="../../how-to/screenshot-a-div/#prepare-the-element">Choose the capture root</a><a class="related-card" href="../../how-to/html-to-png/">HTML to PNG</a><a class="related-card" href="../react/">SnapDOM in React</a><a class="related-card" href="../../docs/api/">Export API</a></div></section>
<section class="cta-section"><div class="cta-card"><h2 id="capture-it-in-the-browser">Capture, modify and convert any element</h2><p>Install SnapDOM and export the DOM to PNG, SVG, PDF or any other format.</p><a href="../../showcase/" class="btn btn-primary">Open the demo</a><a href="../../docs/" class="btn btn-ghost" target="_blank" rel="noopener">Installation</a></div></section>
</div>
<nav class="pager" aria-label="Pages"><a href="../svelte/"><span>Previous</span><strong>Svelte</strong></a><a class="is-next" href="../lit/"><span>Next</span><strong>Lit</strong></a></nav>
</article>
</main>
<footer class="site-foot"><nav class="site-foot-directory" aria-label="Site directory"><div><h2>Learn</h2><a href="../../docs/">Documentation</a><a href="../../docs/#quick-start">Quick start</a><a href="../../how-to/">How-to recipes</a><a href="../../how-to/#frameworks">Framework guides</a><a href="../../compare/">Compare</a></div><div><h2>Explore</h2><a href="../../showcase/">Showcase</a><a href="../../showcase/#plugins">Plugins</a><a href="../../docs/plugins/#build">Writing plugins</a><a href="../../showcase/#labs">Labs</a><a href="../../blog/">Blog</a></div><div><h2>Community</h2><a href="../../ecosystem/">Ecosystem</a><a href="../../made-with/">Made with</a><a href="../../ecosystem/#community-plugins">Community plugins</a><a href="https://github.com/zumerlab/snapdom">GitHub</a><a href="https://www.npmjs.com/package/@zumer/snapdom">npm</a><a href="https://github.com/sponsors/tinchox5">Sponsors</a></div></nav><div class="site-foot-legal"><p>MIT © <a href="https://zumerlab.com/">Zumerlab</a></p></div></footer>
</body>
</html>