Wrap the chart, wait for it to finish drawing, then const img = await snapdom.toPng(wrapper). You get one PNG with the canvas and everything around it.
The problem
Chart.js renders inside a <canvas> element. Its native chart.toBase64Image() exports exactly that canvas — the plotting area — and drops anything you laid out in HTML around it: a heading, a custom legend, annotations, the card background. If your export needs to look like the chart your users actually see on the page, you need to capture the surrounding DOM as well.
snapDOM captures a DOM subtree — including live <canvas> pixels — into a single image, so the chart and its frame come out as one composited PNG.
Step 1 — Install
npm install @zumer/snapdom chart.js
Without a bundler, load both from a CDN:
import { snapdom } from "https://unpkg.com/@zumer/snapdom/dist/snapdom.mjs"; import Chart from "https://cdn.jsdelivr.net/npm/chart.js/auto/+esm";
Step 2 — Wrap the chart in a capture target
Put the canvas inside an element that also holds whatever you want in the exported image:
<div id="chart-card" style="padding:20px;background:#fff;border-radius:12px;"> <h3>Monthly revenue</h3> <canvas id="chart"></canvas> </div>
Step 3 — Wait for the draw, then capture
A canvas only holds pixels once Chart.js has painted them. Capturing during the entry animation gives you a blank or half-drawn plot. The reliable fix is to capture from the animation's onComplete callback:
import { snapdom } from '@zumer/snapdom'; import Chart from 'chart.js/auto'; const chart = new Chart(document.getElementById('chart'), { type: 'bar', data: { labels: ['Jan', 'Feb', 'Mar', 'Apr'], datasets: [{ label: 'Revenue', data: [12, 19, 9, 22] }] }, options: { animation: { onComplete: async () => { const img = await snapdom.toPng(document.getElementById('chart-card')); document.body.appendChild(img); } } } });
Prefer a static export with no timing games? Set options.animation = false and capture right after new Chart(...) returns.
Step 4 — Download it as a file
const result = await snapdom(document.getElementById('chart-card')); await result.download({ format: 'png', filename: 'revenue-chart' });
Or get a Blob to upload:
const blob = await snapdom.toBlob(document.getElementById('chart-card'));
Expected result
A single PNG that matches what's on screen: the <h3> heading, the card's white background and rounded corners, and the fully rendered bar chart — all in one image at your device pixel ratio. Pass { scale: 2 } to toPng for a sharper, higher-resolution export.
When to use snapDOM (and when not to)
- Use snapDOM when the exported image needs the chart plus its HTML surroundings — title, custom legend, annotations, card styling — as one picture.
- Use Chart.js
toBase64Image()when you only need the bare plotting canvas. It's built in, synchronous and lighter — reach for snapDOM only when you actually need the DOM around the canvas.
Known limitations
- Timing. The canvas must be fully painted before capture. Use the
onCompletecallback or disable animation — see Step 3. - Cross-origin images taint the canvas. If your chart draws images (logos, point images) loaded from another origin without CORS headers, the browser marks the canvas tainted and it cannot be read into an image. Serve those assets same-origin or with
Access-Control-Allow-Origin, or route them through a proxy. - WebGL charts. This recipe covers Chart.js's default 2D canvas renderer. WebGL-backed charts may need
preserveDrawingBuffer: trueto be readable at capture time.
Frequently asked questions
My exported chart is blank — why?
You captured before Chart.js finished drawing. Capture inside options.animation.onComplete, or set options.animation = false so the chart paints synchronously.
Can I export just the canvas without the surrounding HTML?
Yes — that's exactly what Chart.js's own chart.toBase64Image() does, and it's the better tool for that job. Use snapDOM when you want the DOM around the canvas included too.
How do I get a higher-resolution image?
Pass { scale: 2 } (or higher) to snapdom.toPng. snapDOM also respects window.devicePixelRatio automatically.
Does this work with react-chartjs-2?
Yes. Put a ref on the wrapping element and call snapdom.toPng(ref.current) from the chart's onComplete option. The wrapper is just a DOM node — snapDOM doesn't care how it was rendered.
Try the live demo
snapDOM captures a live <canvas> in the browser — see it running before you install.