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.
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); 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