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.
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.
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 InstallationHand 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.