October 2026 Workspaces: invite your team

Charts and dynamic images in email

Canvas and inline SVG don't survive email clients. Server-rendered PNG is the only reliable option.

The problem

Every mainstream charting library (Chart.js, D3, Plotly, Recharts) renders into a <canvas> element or outputs inline <svg>. Neither survives in email.

<canvas> requires JavaScript to paint. Email clients don't run JavaScript, so the canvas element renders as a blank rectangle.

Gmail strips <svg> from the email body entirely. Classic Outlook's Word renderer doesn't support inline SVG at all. Apple Mail and modern clients do handle SVG, but targeting one client at the expense of the others isn't viable.

The only image format that works in every email client is a plain <img> tag pointing to a raster image (PNG or JPG). So the only reliable way to include a chart in an email is to render it server-side and serve it as a PNG.

Why it's hard

Email charts are usually data-driven. A weekly digest shows last week's numbers. A receipts email shows a spend breakdown. A notification includes a usage chart. The data is different every time, so you can't use a static image. The chart has to be rendered on demand from the runtime payload.

That means adding a server-side rendering step to your email pipeline: take the data, draw the chart, produce a PNG, host it somewhere, then embed the URL in the HTML before sending. It's several moving parts that your web chart library handles for free in a browser.

The manual solution

Option 1: QuickChart.io (simplest)

QuickChart is a hosted chart-rendering API. You encode a Chart.js config as a URL parameter and get a PNG back. No server-side setup required: just build the URL and put it in an <img> tag:

// Build a QuickChart URL with your chart config
function chartUrl(labels: string[], values: number[]): string {
  const config = {
    type: "bar",
    data: {
      labels,
      datasets: [{ label: "Revenue", data: values, backgroundColor: "#6e7ef0" }],
    },
    options: { plugins: { legend: { display: false } } },
  };
  const encoded = encodeURIComponent(JSON.stringify(config));
  return `https://quickchart.io/chart?c=${encoded}&w=600&h=300&bkg=white`;
}

// Use in your email template (Resend example)
const html = `
  <img
    src="${chartUrl(["Jan", "Feb", "Mar"], [1200, 980, 1540])}"
    width="600"
    height="300"
    alt="Revenue: Jan 1200, Feb 980, Mar 1540"
    style="display:block; max-width:100%;"
  />
`;

Option 2: Node.js with Chart.js + canvas (self-hosted)

For full control, render charts server-side using Chart.js with the canvas npm package (a native Node.js canvas implementation). Render the chart to a PNG buffer, upload it to your CDN, then embed the URL:

// Server-side rendering with Chart.js + canvas
// npm install chart.js canvas

import { createCanvas } from "canvas";
import { Chart } from "chart.js/auto";

async function renderChartPng(
  labels: string[],
  values: number[],
): Promise<Buffer> {
  const canvas = createCanvas(600, 300);
  const ctx = canvas.getContext("2d");

  new Chart(ctx as unknown as CanvasRenderingContext2D, {
    type: "bar",
    data: {
      labels,
      datasets: [{ label: "Revenue", data: values, backgroundColor: "#6e7ef0" }],
    },
    options: { animation: false, plugins: { legend: { display: false } } },
  });

  return canvas.toBuffer("image/png");
}

// Use it in your render handler
const pngBuffer = await renderChartPng(["Jan", "Feb", "Mar"], [1200, 980, 1540]);
// Upload to S3 / GCS / your CDN, then embed the URL as an <img> src

Embedding the PNG

Either way, the result is the same: a hosted PNG URL that you embed with a standard <img> tag. Always include explicit width and height to prevent layout shift while the image loads:

<!-- Embed the server-rendered PNG as a standard <img> -->
<!-- Works in Gmail, Apple Mail, Outlook, and every other client -->
<img
  src="https://your-cdn.com/charts/abc123.png"
  width="600"
  height="300"
  alt="Revenue: Jan $1,200 · Feb $980 · Mar $1,540"
  style="display:block; max-width:100%; border:0;"
/>

<!-- Always include a text fallback for image-blocking clients -->
<p style="font-size:13px; color:#666; margin-top:8px;">
  Revenue: Jan $1,200 · Feb $980 · Mar $1,540
</p>

Gotchas to watch for

  • Many clients block images by default. Corporate Outlook users often see a broken image icon until they click "Download pictures." Write a descriptive alt attribute that summarizes the chart data, and include a short text fallback below the image with the key numbers.
  • QuickChart URLs get long. A complex chart config can exceed URL length limits in some clients. Use QuickChart's short URL API to exchange a long URL for a short one before embedding.
  • Chart data is frozen at send time. Unlike a web dashboard that refreshes, the chart in an email reflects the data at the moment the email was rendered. Don't link to a "live" chart image that changes after the email is sent. The recipient who opens it a week later will see different numbers than the ones discussed in the email body. Cache the PNG and keep it stable.
  • Make chart URLs permanently cacheable. Include the data as part of the URL (or a hash of it) so the same data always produces the same URL. Serve the image with a long Cache-Control: immutable header. Email clients and corporate proxies aggressively cache image URLs. A URL that changes content without changing is unreliable.

Sources

How Postcraft handles this

In the visual editor, Postcraft includes a dedicated Chart block. Pick a chart type, set a static data table or bind a context key from your API payload, choose a color, and the block handles the rest. No code required.

In code mode, the same functionality is available as a {{chart}} Handlebars helper. Pass your runtime data directly from the API payload, set the chart type and options, and Postcraft builds a versioned chart spec and bakes it into a server-rendered PNG served from its own endpoint.

Either way, on a target=email render call the chart comes out as a standard <img> tag in the HTML. No CDN to set up, no separate rendering step. Supports bar, horizontal bar, line, pie, and segmented (one bar split into proportional colored segments). Chart URLs are versioned and permanently cacheable: an email you sent two years ago still renders its chart correctly.

<!-- In a Postcraft template, use the {{chart}} helper -->
<!-- Data comes from your runtime API payload -->
{{chart
  type="bar"
  data=monthlyRevenue
  labels="month"
  values="revenue"
  title="Monthly revenue"
  color="#6e7ef0"
  width=600
  height=300
}}

<!-- Or with inline data -->
{{chart type="pie" data='[{"label":"Free","value":72},{"label":"Paid","value":28}]'}}

<!-- Parts of a whole as one segmented bar -->
{{chart type="segmented" data='{"New":5,"Escalating":1,"Regressed":1,"Ongoing":22}'}}
Try it free