Await this.updateComplete, query the card inside this.renderRoot, and pass that element to SnapDOM. A parent’s promise does not automatically wait for reactive child elements.
Capture one region inside a Lit element
The button and card share a shadow root here. Querying #card captures the card without including the download control.
npm install @zumer/snapdom
import { LitElement, css, html } from 'lit'
import { snapdom } from '@zumer/snapdom'
class ShareCard extends LitElement {
static styles = css`
article { padding: 24px; border-radius: 16px; background: white; }
`
async save() {
await this.updateComplete
const card = this.renderRoot.querySelector('#card')
if (!(card instanceof HTMLElement)) return
await snapdom.download(card, {
format: 'png',
filename: 'lit-card.png',
scale: 2,
})
}
render() {
return html`
<article id="card">
<h2>Quarterly report</h2>
<p>Rendered inside Lit Shadow DOM.</p>
</article>
<button type="button" @click=${this.save}>Download PNG</button>
`
}
}
customElements.define('share-card', ShareCard)
Lit’s default renderRoot is an open shadow root. SnapDOM preserves its computed styles, CSS variables, pseudo-elements, and open Shadow DOM. No light-DOM copy is needed.
Capture the host or the internal card
Pass this to capture the custom-element host and its open shadow tree. Pass the queried #card when the host’s margins, slot layout, or controls should stay out of the image. Starting from the smallest meaningful root also avoids processing unrelated nodes.
// Host box plus its open shadow tree
await snapdom.toPng(this)
// Only the internal export surface
const card = this.renderRoot.querySelector('#card')
if (card) await snapdom.toPng(card)
What updateComplete does not wait for
this.updateComplete resolves after this element’s update. It does not, by default, wait for every Lit descendant. If a reactive child draws the chart being exported, await that child too.
await this.updateComplete
const chart = this.renderRoot.querySelector('sales-chart')
if (chart?.updateComplete) await chart.updateComplete
const card = this.renderRoot.querySelector('#card')
if (card) await snapdom.download(card, { filename: 'report.png' })
For an unknown descendant tree, waiting for the next requestAnimationFrame lets Lit’s pending microtasks drain before the browser paints. Neither wait finishes network requests: await the data promise, image decode(), or document.fonts.ready when those resources alter layout.
Lit-specific failure modes
- The query returns null: the method ran before the first update, or a conditional template did not render the target. Await
updateCompleteand keep the null guard. - A nested chart is stale: the parent completed while the child still had an update queued. Await the child’s
updateComplete. - The host capture misses internals: SnapDOM can traverse an open shadow root, but the browser does not expose a closed one through
element.shadowRoot. Capture a known internal node if your own component deliberately creates a closed root.
Lit SSR boundary
Lit can render templates on the server, but SnapDOM needs the client’s layout and computed styles. Run the capture after the custom element connects and updates in the browser. A click handler already satisfies that boundary.
Frequently asked questions
Does SnapDOM have a Lit-specific package?
No. Import @zumer/snapdom in the Lit element and pass it either the host or an element from renderRoot.
Does updateComplete wait for child components?
Not by default. It resolves when the current element finishes updating. Await a known child’s updateComplete too, or wait for the next animation frame when the capture depends on a descendant tree.
Can SnapDOM capture a Lit shadow root?
Yes. SnapDOM captures open Shadow DOM and its scoped styles. Query inside renderRoot when you want only one internal region instead of the entire host.
Export a Lit render root
Choose the host or one node inside renderRoot, wait for its update, and pass that DOM element to SnapDOM.