Bind a template ref with ref="cardRef", install @zumer/snapdom, and call snapdom.toPng(cardRef.value) from a button handler. SnapDOM returns an HTMLImageElement ready to append.
1. Install
npm install @zumer/snapdom
Zero runtime dependencies, ESM-only. Works with Vite, Vue CLI and Nuxt out of the box.
2. Minimal capture component (Composition API)
<script setup> import { ref } from 'vue'; import { snapdom } from '@zumer/snapdom'; const cardRef = ref(null); async function capture() { if (!cardRef.value) return; const img = await snapdom.toPng(cardRef.value, { scale: 2 }); document.body.appendChild(img); } </script> <template> <div> <div ref="cardRef" class="card"> <h3>Hello SnapDOM</h3> <p>This whole div will be captured.</p> </div> <button @click="capture">Capture</button> </div> </template>
That's the entire component. The template ref name (cardRef) must match the variable declared in the script setup.
Try it live
This is the component above, running on this page. The button fires snapdom.toPng(cardRef.value) and the captured PNG appears below. SnapDOM only sees the DOM node, so the call is identical whether Vue, React or plain HTML rendered it.
Hello SnapDOM
This whole div will be captured.
3. Download instead of appending
async function download() { if (!cardRef.value) return; const result = await snapdom(cardRef.value); await result.download({ format: 'png', filename: 'card' }); }
4. Render a live preview
Capture to a Blob and create an object URL to use as the preview <img> source:
<script setup> import { ref } from 'vue'; import { snapdom } from '@zumer/snapdom'; const cardRef = ref(null); const previewUrl = ref(''); async function capture() { if (!cardRef.value) return; const blob = await snapdom.toBlob(cardRef.value, { type: 'png' }); previewUrl.value = URL.createObjectURL(blob); } </script> <template> <div ref="cardRef">…</div> <button @click="capture">Capture</button> <img v-if="previewUrl" :src="previewUrl" alt="preview" /> </template>
Revoke the object URL with URL.revokeObjectURL(previewUrl.value) when you're done with it to avoid leaks.
5. Reusable composable
// useSnapdom.js import { ref } from 'vue'; import { snapdom } from '@zumer/snapdom'; export function useSnapdom(options = {}) { const target = ref(null); async function capture(format = 'png') { if (!target.value) return null; const result = await snapdom(target.value, options); return result.to(format); } return { target, capture }; }
<script setup> import { useSnapdom } from './useSnapdom'; const { target, capture } = useSnapdom({ scale: 2, embedFonts: true }); </script> <template> <div ref="target">…</div> <button @click="capture('png')">Capture</button> </template>
Common gotchas
The captured image is blank
Make sure you trigger the capture from a user event (click) or inside onMounted, never directly inside setup(). The element ref is only assigned after the component has been mounted.
Also confirm the captured element isn't v-if'd off the page. SnapDOM needs the element to be in the live DOM tree.
Web fonts aren't being captured
Pass { embedFonts: true }. SnapDOM will inline the matching @font-face declarations into the SVG so the captured image renders with your real fonts.
How do I use SnapDOM with the Options API?
Bind ref="cardRef" in the template and access this.$refs.cardRef in a method:
const img = await snapdom.toPng(this.$refs.cardRef);
Does SnapDOM work with Nuxt?
Yes, but only on the client side. Wrap your capture component in <ClientOnly> or import SnapDOM dynamically inside onMounted so it never runs on the server.
Cross-origin images don't render
Use the useProxy option pointing to a CORS proxy: { useProxy: 'https://your-proxy.example.com/' }.
Try it in your project
Drop SnapDOM into your Vue app and capture your first component in under a minute.
Open the demo Install from npm