React Guide
zumerlab/snapdom

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

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.

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 a data 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);
    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>
  </>
);

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

Pass { embedFonts: true } in the options. SnapDOM will inline the matching @font-face declarations into the SVG output so the captured image uses your real fonts.

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 in under a minute.

Open the demo Install from npm