In the browser, chart.toBase64Image() returns the chart as a PNG data URL, which you can download or upload. For a good export, wait for the animation to finish, raise devicePixelRatio for a sharp image, and add a background, because the canvas is transparent. On a server, chartjs-node-canvas renders the same configuration to a PNG buffer without a browser.

Everything here was tested on October 14, 2026 with Chart.js 4.5.1 (the current major version) in headless Chromium, and with chartjs-node-canvas 5.0.0 on Node.js 24.

In the browser

Chart.js draws on a <canvas>, so exporting means reading the canvas’s pixels. Two ways:

// A data URL: "data:image/png;base64,..."
const url = chart.toBase64Image();

// A Blob, better for large images and uploads
chart.canvas.toBlob((blob) => {
  const link = document.createElement('a');
  link.href = URL.createObjectURL(blob);
  link.download = 'chart.png';
  link.click();
  setTimeout(() => URL.revokeObjectURL(link.href), 0);
}, 'image/png');

toBase64Image(type?, quality?) “returns a base 64 encoded string of the chart in its current state”; pass 'image/jpeg', 1 for a JPEG. canvas.toBlob() produces a PNG by default too, and MDN recommends it over toDataURL(), which “encodes the whole image in an in-memory string”.

Four things that spoil an export

1. Exporting mid-animation

“In its current state” means exactly that: call it while the chart is still animating in and you get a half-drawn chart. Either export when the animation completes, or turn animation off for charts you only export:

const chart = new Chart(canvas, {
  type: 'bar',
  data,
  options: {
    animation: {
      onComplete: () => {
        const url = chart.toBase64Image();
        // ...download or upload it
      },
    },
  },
});

// Or, for export-only charts:
// options: { animation: false }

onComplete is “called when all animations are completed”, and animation: false “disables all animations”. Animations also run when the chart updates (new data, a resize), so onComplete can fire more than once; guard it if you only want one export.

2. A transparent background

A canvas starts transparent, and Chart.js only draws the chart on it. The PNG looks fine on a white page and broken on a dark one: in a dark-mode email, a Slack dark theme or a slide. Chart.js has “no built-in support” for a background; its documentation gives a small plugin instead. Here it is with the color fixed to white:

const whiteBackground = {
  id: 'whiteBackground',
  beforeDraw(chart) {
    const { ctx } = chart;
    ctx.save();
    ctx.globalCompositeOperation = 'destination-over';
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, chart.width, chart.height);
    ctx.restore();
  },
};

new Chart(canvas, { type: 'bar', data, options, plugins: [whiteBackground] });

destination-over paints the background behind whatever is already drawn, so the plugin’s position in the drawing order doesn’t matter.

3. A blurry image

By default the canvas uses the screen’s pixel ratio: 1 on a standard monitor, 2 or 3 on a phone or a high-density laptop. Export the same chart from two machines and you get two different image sizes. Set it explicitly with the devicePixelRatio option, which “will force the canvas size to be scaled by that amount, relative to the container size”:

options: { devicePixelRatio: 2 }

In our test, a chart in a 600×300 container with devicePixelRatio: 2 exported as a 1200×600 PNG of 32 KB. Use 2 for screens, email and Slack; for print, see charts in PDFs.

The export size follows the container size. For a fixed size regardless of the page layout, give the chart a fixed-size container, or call chart.resize(width, height) before exporting and restore it afterwards.

4. The wrong font

If the chart uses a web font that hasn’t loaded yet, it is drawn with the fallback font, and the export keeps it. Load the font explicitly before creating the chart, or call chart.update() once it arrives:

await document.fonts.load('12px Inter'); // the family and size your chart uses
const chart = new Chart(canvas, config);

document.fonts.load() fetches the font and resolves when it is ready. Waiting for document.fonts.ready is not enough on its own: it resolves once the fonts the page has asked for are loaded, and in our test in Chromium 156 a font used only by a canvas had not been asked for yet. Chart.js’s own font documentation notes that charts using a font that isn’t loaded yet “will need to be updated once the font is loaded”.

And one error: the tainted canvas

If the chart draws an image from another origin (a logo, a point style) that wasn’t served with CORS headers, the canvas becomes “tainted” and every export throws a SecurityError (MDN). Serve such images from your own origin, or with Access-Control-Allow-Origin and crossorigin="anonymous".

All together, tested

This is the browser test we ran: a 600×300 bar chart with a white background, exported at 2× once the animation completes.

<div style="width: 600px; height: 300px"><canvas id="signups"></canvas></div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"
  integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous"></script>
<script>
  const whiteBackground = {
    id: 'whiteBackground',
    beforeDraw(chart) {
      const { ctx } = chart;
      ctx.save();
      ctx.globalCompositeOperation = 'destination-over';
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(0, 0, chart.width, chart.height);
      ctx.restore();
    },
  };

  const chart = new Chart(document.getElementById('signups'), {
    type: 'bar',
    data: {
      labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
      datasets: [{ label: 'Signups', data: [120, 190, 170, 240, 210], backgroundColor: '#2b3cd8' }],
    },
    options: {
      devicePixelRatio: 2,
      animation: {
        onComplete: () => {
          chart.canvas.toBlob((blob) => console.log(blob.type, blob.size, chart.canvas.width, chart.canvas.height));
        },
      },
    },
    plugins: [whiteBackground],
  });
</script>

It logged image/png 32390 1200 600.

On a server, with Node.js

For scheduled reports, emails and bots there is no browser to draw in. chartjs-node-canvas renders Chart.js configurations with node-canvas 3.x, a Cairo-backed implementation of the canvas API:

npm install chart.js chartjs-node-canvas
// render-chart.mjs
import { writeFile } from 'node:fs/promises';
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';

const renderer = new ChartJSNodeCanvas({ width: 800, height: 400, backgroundColour: 'white' });

const png = await renderer.renderToBuffer({
  type: 'bar',
  data: {
    labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
    datasets: [{ label: 'Signups', data: [120, 190, 170, 240, 210], backgroundColor: '#2b3cd8' }],
  },
  options: { devicePixelRatio: 2 },
});

await writeFile('signups.png', png);
console.log(`signups.png: ${png.length} bytes, ${png.readUInt32BE(16)}x${png.readUInt32BE(20)} px`);
$ node render-chart.mjs
signups.png: 23217 bytes, 1600x800 px

What the library handles for you, and what it doesn’t:

  • Animation is off: “Chart animation (and responsive resize) is disabled by this library”, so there is no partial frame to worry about.
  • Background: backgroundColour fills the otherwise transparent canvas; leave it out and the PNG is transparent.
  • Fonts must be registered before the canvas is created, which happens at each render: call renderer.registerFont('./Inter.ttf', { family: 'Inter' }) before renderToBuffer, and name the family in your chart’s font options. Without it, text falls back to whatever fonts the server has, which on a minimal container may be very few.
  • Native code: node-canvas 3.x downloads prebuilt binaries for macOS, Linux x64 (glibc) and Windows x64; elsewhere, including Alpine Linux, it compiles from source and needs Cairo and Pango installed. (Version 4, in pre-release as of October 2026, changes the build.) @napi-rs/canvas, a Skia-based alternative with no system dependencies, is worth a look if that’s a problem.

The alternative is a headless browser (Playwright or Puppeteer) that loads a page with the chart and screenshots it. It renders exactly what users see, at the cost of running Chromium on the server.

Or skip the code

If you need a chart image rather than a pipeline, Chart Link does the export for you: paste CSV or JSON, preview it, and download a PNG or SVG with a solid background and a sensible size. It runs in your browser, so the data stays on your machine.

Sources

All sources were checked on October 14, 2026.