SnapDOMGitHub8K
Vue 3 Guide
zumerlab/snapdom

Capture HTML to PNG in Vue 3

A copy-paste single-file component using the Composition API, plus the gotchas to watch out for.

TL;DR

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.

The captured PNG will appear here.

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