SnapDOMGitHub★ 8K
Live demo

Live element

Component preview
Capture an editable component
1,284+12% vs last week

Editable text and an open shadow root can be captured together.

Result

Nothing captured yet

Edit the title, then run. This uses the real library from unpkg.
React Guide

Capture HTML to PNG in React

A copy-paste hook, the right place in the lifecycle to call it, and the one mistake that produces blank screenshots.

TL;DR

Attach a useRef to the element you want to capture, install @zumer/snapdom, and call snapdom.toPng(ref.current) from a button handler. SnapDOM returns the HTMLImageElement ready to append.

1. Install

npm install @zumer/snapdom@latest

SnapDOM has zero runtime dependencies and ships as an ES module — works with Vite, Create React App, Parcel, esbuild and any modern bundler out of the box.

2. Minimal capture component

import { useRef } from 'react';
import { snapdom } from '@zumer/snapdom';

export function CaptureCard() {
  const cardRef = useRef(null);

  const handleCapture = async () => {
    if (!cardRef.current) return;
    const img = await snapdom.toPng(cardRef.current, { scale: 2 });
    document.body.appendChild(img);
  };

  return (
    <div>
      <div ref={cardRef} className="card">
        <h3>Hello SnapDOM</h3>
        <p>This whole div will be captured.</p>
      </div>
      <button onClick={handleCapture}>Capture</button>
    </div>
  );
}

That's the entire integration. Click the button, get a PNG appended at the bottom of the document.

Try it live

This is the component above, running on this page. The button fires snapdom.toPng(cardRef.current) and the captured PNG appears below. SnapDOM only sees the DOM node, so the call is identical whether React, Vue or plain HTML rendered it.

Hello SnapDOM

This whole div will be captured.

The captured PNG will appear here.

3. Download instead of appending

Most apps want to download the image rather than insert it. SnapDOM exposes a one-liner:

const handleDownload = async () => {
  if (!cardRef.current) return;
  const result = await snapdom(cardRef.current);
  await result.download({ format: 'png', filename: 'card' });
};

4. Keep the captured image in state

If you want to render a preview inside your component, store the image as an object URL using toBlob + URL.createObjectURL:

import { useRef, useState } from 'react';
import { snapdom } from '@zumer/snapdom';

export function CapturePreview() {
  const cardRef = useRef(null);
  const [previewUrl, setPreviewUrl] = useState(null);

  const handleCapture = async () => {
    if (!cardRef.current) return;
    const blob = await snapdom.toBlob(cardRef.current, { format: 'png' });
    setPreviewUrl(URL.createObjectURL(blob));
  };

  return (
    <>
      <div ref={cardRef}>…</div>
      <button onClick={handleCapture}>Capture</button>
      {previewUrl && <img src={previewUrl} alt="preview" />}
    </>
  );
}

Remember to revoke the object URL with URL.revokeObjectURL(previewUrl) when you're done with it to avoid leaks.

5. Reusable React hook

If you call SnapDOM from many components, extract a small hook:

// useSnapdom.js
import { useCallback, useRef } from 'react';
import { snapdom } from '@zumer/snapdom';

export function useSnapdom(options = {}) {
  const ref = useRef(null);

  const capture = useCallback(async (format = 'png') => {
    if (!ref.current) return null;
    const result = await snapdom(ref.current, options);
    return result.to(format);
  }, [options]);

  return { ref, capture };
}

Use it like this:

const { ref, capture } = useSnapdom({ scale: 2, embedFonts: true });

return (
  <>
    <div ref={ref}>…</div>
    <button onClick={() => capture('png')}>Capture</button>
  </>
);

Export a real component

Use the React component export recipe when choosing the capture root and keeping controls outside it. For several widgets together, use the dashboard recipe. For a file to upload, adapt the PNG Blob and FormData example after the ref’s content has finished rendering.

Common gotchas

My captured image is blank or empty

The two most common causes:

1. You called snapdom before the ref was attached. Always trigger the capture from a user event (button click) or inside useEffect after mount, never directly inside the component body.

2. The captured element has display: none or is detached from the document. SnapDOM needs the element to be in the live DOM tree.

Web fonts render as Times New Roman

SnapDOM v3.x.x defaults to embedFonts: 'auto' and embeds used web fonts when their files are accessible. Wait for application data and fonts to settle before capture if they can change the layout.

Cross-origin images don't render

SnapDOM fetches external images and inlines them as data URLs. If your CDN doesn't send Access-Control-Allow-Origin, configure a proxy with { useProxy: 'https://your-proxy.example.com/' }.

Does SnapDOM work with React 18 strict mode?

Yes. SnapDOM doesn't subscribe to React lifecycle, so the double-invocation in development strict mode doesn't affect captures triggered from event handlers.

Can I use SnapDOM in Next.js?

Yes — use a client component ('use client') and a dynamic import. See the dedicated Next.js guide.

Does SnapDOM work with React Native?

No. SnapDOM is a browser-only library — it relies on DOM APIs and SVG foreignObject that don't exist in React Native.

Try it in your project

Drop SnapDOM into your React app and capture your first component from a click handler.

Open the demo Installation

Hand off and verify a React component

After waiting for the component to settle, use the Claude Code capture skill for refactors, or export editable artwork for a design review.