SnapDOMGitHub8K
Framework Guide · Lit 3

Capture a Lit component

Choose the host or one node inside renderRoot, then wait for the reactive updates that own that exact subtree.

TL;DR

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

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.

Open the demoInstall from npm