Guide for developers

Charts in emails

Email clients do not run JavaScript, and SVG support in Gmail, Outlook and Apple Mail is partial. A PNG loaded from a URL works in all three. This guide covers what to know before you send one.

Sources read on 2 Nov 2026. Each fact about an email client links to its source (S1, S2...), listed at the end with a short quote.

The short version

Why a PNG, not JavaScript or SVG

Chart libraries draw in the browser with JavaScript. Email clients in general do not run it. Litmus puts it plainly: "avoid JavaScript at all costs, as it is unsupported due to security issues" S8.

SVG looks like the fix, but support depends on the client, the version and how you include it. This is what Can I Email lists for the three clients most readers use. Test dates are in the sources, and some are old.

FeatureGmailOutlookApple Mail
PNG image S1SupportedSupportedSupported
SVG as a linked image S2Partial. The 2026 entries say the image is shown but rasterized to PNG. Older entries: no support in webmail, partial on iOS and Android.Depends on the version. Windows 2007 to 2016: no. Windows 2019, Windows Mail, Outlook.com: yes. Outlook for iOS: no in 2020, yes in 2023.macOS 13 and iOS 13, 14: no. macOS 14 and iOS 15, 26: yes.
Inline svg element S3No, on desktop, iOS, Android and mobile webmail.No on Windows, Outlook.com, iOS and Android. Mixed on macOS (2016 yes, 16.80 no).macOS 13: buggy, needs a body background. iOS 13: yes.
Base 64 data URI S4No.No on Windows (2019: partial). Yes on Outlook.com, iOS, Android and macOS.Yes.
picture and srcset S7No. The tags are replaced.No on Windows, Outlook.com, iOS and Android. Mixed on macOS.Yes, partial on recent macOS.
prefers-color-scheme S5No.No on Windows. Yes on macOS, Outlook.com and iOS.Yes.

Source for each row: Can I Email, which also publishes the data as JSON. The Gmail SVG row was last tested in September 2026. The PNG, inline svg and Base 64 rows were last tested in February 2020.

So SVG might work for some readers and fail for others, and you cannot tell which when you send. PNG is the one format every listed Gmail, Outlook and Apple Mail entry supports.

Why a PNG by URL works

An <img src="https://..."> pointing at an image endpoint is the oldest trick in email. The server draws the chart and returns image/png. The email client loads it like any other picture.

Two things decide whether the reader sees it:

You can avoid blocked external images by attaching the PNG to the message and pointing theimg at it with a cid: address. Nodemailer notes that many clients block external images by default, and that embedded ones show without a click S16. The trade-offs are real:

For most dashboards and reports, a URL is simpler. Use CID when the numbers must show even if the reader never allows images, or when the chart contains data you do not want on a public URL.

Gmail's image proxy and caching

Since December 2013, Gmail loads images through Google's own proxy instead of the sender's serverS10. A consultancy that watched the change reported that the proxy fetches an image the first time and then keeps it in a cache S11. We found no Google page that states how long the cache lasts.

What this means for you, as practical rules and not as documented guarantees:

ChartPNG returns the same image for the same URL and tells caches they may keep it, so a URL that encodes the full config is a good fit for this.

Retina: dpr=2 and the size attributes

On a high-density screen a 600 pixel image looks soft. The usual fix is an image at least double the display size, shown smaller with the width and height attributesS13. With ChartPNG that is one parameter:

/chart?c=...&w=600&h=360&dpr=2

w and h are the size on screen. dpr=2 draws it at 1200 by 720 pixels. Then put the on-screen size in the tag: width="600" height="360". A bar chart like the one below weighs about 20 KB at that size (measured on our own render code, 2 Nov 2026).

Two cautions:

<img src="..." width="600" alt="..."
  style="width: 100%; max-width: 600px; display: block; border: 0;" />

Dark mode and transparent backgrounds

Gmail does not support prefers-color-scheme S5, so you cannot swap a light chart for a dark one with a media query there. You send one image, and it has to work on both a light and a dark inbox. Litmus says the same about image swapping in clients with limited dark mode support S14.

A transparent PNG (bkg=transparent) lets the email background show through. It also puts the chart text on a background you do not control. Litmus advises adding a translucent outline to transparent PNGs that have dark text S14.

We checked our own output. The default ChartPNG text colors are made for light backgrounds, so on a dark background the labels are faint and the grid lines turn bright. Both images below are the same chart:

Light inbox, transparent PNG

Bar chart of signups, Monday 12, Tuesday 19, Wednesday 7, with a transparent background on a white panel.

Dark inbox, same PNG

The same bar chart on a near black panel. The axis labels are faint and the grid lines are bright.

The safer default is an opaque background (bkg=white or your brand color) with a little padding. The chart then stays a light card inside a dark email, which is readable, and no client has to guess how to recolor it. Litmus gives the same advice for icons: put them on a background so they still work where image swapping is not supported S14.

Light inbox, opaque PNG

Bar chart of signups, Monday 12, Tuesday 19, Wednesday 7, on a white background.

Dark inbox, same PNG

The same bar chart on a white card inside a near black panel.

If you do use a transparent background, set text and grid colors in the chart config that read on both light and dark backgrounds, and test in the clients your readers use. We did not test how each client recolors images in its dark mode.

Alt text

A chart is the part of the message people most want to see, and it is an image, so it is the first thing a blocked-images setting removes, and classic Outlook for Windows blocks by default S12. Write the alt text as the answer, not as a label:

Put the key number in the text of the email as well. Then the message still makes sense without the image. Style the alt text with an inline font-family, font-size andcolor on the img, as in the examples, so it is readable when it shows in place of the image.

Size limits and long URLs

Three different limits get mixed up:

When a URL gets long, stop putting the config in the URL. Two options that work today:

  1. POST the config from your server, get the PNG back, and send it as a CID attachment (examples below).
  2. POST, then host the PNG yourself on your storage or CDN, and use that short URL in the email. This also lets you keep the chart if you leave a service.
curl -X POST https://chartpng.dev/chart \
  -H 'Content-Type: application/json' \
  -d '{"c":{"type":"bar","data":{"labels":["Mon","Tue","Wed"],"datasets":[{"data":[12,19,7]}]}},"w":600,"h":360,"dpr":2}' \
  -o chart.png

ChartPNG does not have saved or short configs today, and signed URLs still carry the full config inc. If you need a short URL, host the PNG yourself.

Copy-paste examples

1. HTML for the email body

A single centered table cell, a fixed 600 pixel image drawn at double density, and alt text. The URL is a real ChartPNG URL for the sample chart. Inside an HTML attribute the & is written &amp;. The free plan adds a small watermark.

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
  <tr>
    <td align="center" bgcolor="#ffffff" style="padding: 16px;">
      <img
        src="https://chartpng.dev/chart?c=%7B%22type%22%3A%22bar%22%2C%22data%22%3A%7B%22labels%22%3A%5B%22Mon%22%2C%22Tue%22%2C%22Wed%22%5D%2C%22datasets%22%3A%5B%7B%22label%22%3A%22Signups%22%2C%22data%22%3A%5B12%2C19%2C7%5D%7D%5D%7D%7D&amp;w=600&amp;h=360&amp;dpr=2&amp;bkg=white"
        width="600" height="360"
        alt="Signups per weekday: Mon 12, Tue 19, Wed 7. Tuesday is the peak."
        style="display: block; max-width: 100%; height: auto; border: 0; font-family: sans-serif; font-size: 14px; color: #15161a;"
      />
    </td>
  </tr>
</table>

The same URL with plain & separators, ready to paste into a browser or the Gmail dialog below:

https://chartpng.dev/chart?c=%7B%22type%22%3A%22bar%22%2C%22data%22%3A%7B%22labels%22%3A%5B%22Mon%22%2C%22Tue%22%2C%22Wed%22%5D%2C%22datasets%22%3A%5B%7B%22label%22%3A%22Signups%22%2C%22data%22%3A%5B12%2C19%2C7%5D%7D%5D%7D%7D&w=600&h=360&dpr=2&bkg=white

2. Build the URL in Node

const config = {
  type: 'bar',
  data: { labels: ['Mon', 'Tue', 'Wed'], datasets: [{ label: 'Signups', data: [12, 19, 7] }] },
};

// 600 x 360 on screen, drawn at 1200 x 720 pixels.
const params = new URLSearchParams({ c: JSON.stringify(config), w: '600', h: '360', dpr: '2', bkg: 'white' });
const chartUrl = 'https://chartpng.dev/chart?' + params;
const alt = 'Signups per weekday: Mon 12, Tue 19, Wed 7. Tuesday is the peak.';

// In HTML, & in an attribute is written &amp;.
const img = '<img src="' + chartUrl.replaceAll('&', '&amp;') + '" width="600" height="360" alt="' + alt + '"'
  + ' style="display:block;max-width:100%;height:auto;border:0" />';

3. Send with Resend

Resend takes the HTML in the html parameter S18. With the URL from step 2:

import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

// ...config, chartUrl and img as above.

const { data, error } = await resend.emails.send({
  from: 'Acme <onboarding@resend.dev>',
  to: ['delivered@resend.dev'],
  subject: 'Weekly signups',
  html: '<p>Signups this week:</p>' + img,
});
if (error) console.error({ error });
Embed the PNG instead of linking it (CID)

Resend sets the content ID in contentId, and the same string goes aftercid: in the img S19. Apple Mail on macOS ignored the width attribute on CID images in Can I Email's test S6, so this example also sets the size in CSS (width:600px). We did not test it in Apple Mail.

// Draw the PNG on your server, then send it inside the message.
const res = await fetch('https://chartpng.dev/chart', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ c: config, w: 600, h: 360, dpr: 2, bkg: 'white' }),
});
if (!res.ok) throw new Error('chart failed: ' + res.status);
const png = Buffer.from(await res.arrayBuffer());

const { error } = await resend.emails.send({
  from: 'Acme <onboarding@resend.dev>',
  to: ['delivered@resend.dev'],
  subject: 'Weekly signups',
  html: '<img src="cid:signups-chart" width="600" height="360" alt="' + alt + '" style="display:block;border:0;width:600px;max-width:100%;height:auto" />',
  attachments: [{ filename: 'signups.png', content: png.toString('base64'), contentId: 'signups-chart' }],
});
if (error) console.error({ error });

4. Send with Nodemailer

Create one transporter and call sendMail() with your message S17.

import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: 'smtp.example.com',
  port: 587,
  secure: false,
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});

// ...config, chartUrl and img as above.

await transporter.sendMail({
  from: 'Alice <alice@example.com>',
  to: 'bob@example.com',
  subject: 'Weekly signups',
  html: '<p>Signups this week:</p>' + img,
});
Embed the PNG instead of linking it (CID)

Nodemailer embeds an image when the attachment has a cid and the HTML refers to it with src="cid:...". It only takes effect when the message has an HTML bodyS16. Apple Mail on macOS ignored the width attribute on CID images in Can I Email's test S6, so this example also sets the size in CSS (width:600px). We did not test it in Apple Mail.

// ...res and png from the POST request shown for Resend.

await transporter.sendMail({
  from: 'Alice <alice@example.com>',
  to: 'bob@example.com',
  subject: 'Weekly signups',
  html: '<img src="cid:signups@example.com" width="600" height="360" alt="' + alt + '" style="display:block;border:0;width:600px;max-width:100%;height:auto" />',
  attachments: [{ filename: 'signups.png', content: png, cid: 'signups@example.com' }],
});

5. A manual email in Gmail

For a one-off message you write by hand:

  1. Open the plain chart URL from example 1 in your browser and check the image.
  2. In the Gmail compose window, choose Insert photo, then the Web address (URL) tab.
  3. Paste the same plain URL (with &, not &amp;) and insert it.

We could only confirm this from a third-party guide, dated July 2026 S20. Google's help pages that we read describe dragging an image into the compose window instead, and we did not test it in an account, so labels may differ in your Gmail.

Checklist before you send

What we could not confirm

Sources

All pages read on 2 Nov 2026. Quotes are exact and short. Web pages change, so check the date.

  1. S1Can I Email: PNG image formathttps://www.caniemail.com/features/image-png/

    Quote: "Support for PNG image format"

    Read 2 Nov 2026. Last tested 6 Feb 2020.

  2. S2Can I Email: SVG image format (linked file)https://www.caniemail.com/features/image-svg/

    Quote: "The image is rendered but rasterized in PNG."

    Read 2 Nov 2026. Last tested 16 Sep 2026.

  3. S3Can I Email: embedded svg elementhttps://www.caniemail.com/features/html-svg/

    Quote: "Support for embedded <svg> images."

    Read 2 Nov 2026. Last tested 6 Feb 2020.

  4. S4Can I Email: Base 64 image formathttps://www.caniemail.com/features/image-base64/

    Quote: "Support for Base 64 image format"

    Read 2 Nov 2026. Last tested 6 Feb 2020 (Gmail webmail entry up to May 2024).

  5. S5Can I Email: prefers-color-schemehttps://www.caniemail.com/features/css-at-media-prefers-color-scheme/

    Quote: "This media query allows to theme for system light and dark mode."

    Read 2 Nov 2026. Last tested 8 Mar 2023.

  6. S6Can I Email: width attributehttps://www.caniemail.com/features/html-width/

    Quote: "The `width` attribute is ignored on CID embedded images."

    Read 2 Nov 2026. Last tested 27 Sep 2019. The same page says: "Sizes set in attributes don't scale in 120 dpi mode."

  7. S7Can I Email: picture elementhttps://www.caniemail.com/features/html-picture/

    Quote: "`<picture>` and `<source>` tags are replaced by `<u></u>` tags."

    Read 2 Nov 2026. Last tested 15 Apr 2024. The srcset page (29 May 2019) also lists Gmail as not supported.

  8. S8Litmus: Web vs. HTML Email Designhttps://www.litmus.com/blog/the-tyranny-of-tables-why-web-and-email-design-are-so-different

    Quote: "avoid JavaScript at all costs, as it is unsupported due to security issues"

    Read 2 Nov 2026.

  9. S9Gmail Help: Turn images on or offhttps://support.google.com/mail/answer/145919

    Quote: "you’ll see the image automatically"

    Read 2 Nov 2026. It adds: "you won’t see images automatically" when Gmail thinks a message is suspicious.

  10. S10Official Gmail Blog: Images Now Showing (12 Dec 2013)https://gmail.googleblog.com/2013/12/images-now-showing.html

    Quote: "Gmail will now serve all images through Google’s own secure proxy servers."

    Read 2 Nov 2026. Posted 12 Dec 2013.

  11. S11Word to the Wise: Gmail deploys image proxy servers (5 Dec 2013)https://www.wordtothewise.com/2013/12/gmail-deploys-image-proxy-servers

    Quote: "caches it on a Google managed proxy"

    Read 2 Nov 2026. Posted 5 Dec 2013. A deliverability consultancy describing what it saw, not Google documentation.

  12. S12Microsoft Support: automatic picture downloads in classic Outlook for Windows (Microsoft 365, 2024, 2021, 2019, 2016)https://support.microsoft.com/en-us/outlook/block-or-unblock-automatic-picture-downloads-in-classic-outlook-email-messages

    Quote: "Microsoft Outlook is configured by default to block automatic picture downloads from the Internet."

    Read 2 Nov 2026. The page lists only classic Outlook for Windows and shows no date.

  13. S13Litmus: Understanding Retina Images in HTML Emailhttps://www.litmus.com/blog/understanding-retina-images-in-html-email

    Quote: "anything that’s at least double the intended display size works"

    Read 2 Nov 2026. It also says some Outlook versions "still display the retina images at their full size" and suggests a max-width rule.

  14. S14Litmus: Ultimate Guide to Dark Mode for Emailhttps://www.litmus.com/blog/the-ultimate-guide-to-dark-mode-for-email-marketers

    Quote: "Add a translucent outline to transparent PNGs with dark text for legibility"

    Read 2 Nov 2026.

  15. S15Litmus: How to Keep Gmail from Clipping Your Emailshttps://www.litmus.com/blog/how-to-keep-gmail-from-clipping-your-emails

    Quote: "Gmail clips any message with a file size of over 102 kilobytes (kb)."

    Read 2 Nov 2026. It also says the limit "does not include images".

  16. S16Nodemailer: Embedded imageshttps://nodemailer.com/message/embedded-images

    Quote: "Many email clients block external images by default for privacy and security reasons."

    Read 2 Nov 2026. Also: images are referenced "using the cid: (Content-ID) URL scheme".

  17. S17Nodemailer: Message configurationhttps://nodemailer.com/message

    Quote: "html - The HTML version of the message body."

    Read 2 Nov 2026. Quick start (https://nodemailer.com/): "Call transporter.sendMail() with your message options."

  18. S18Resend: Send emails with Node.jshttps://resend.com/docs/send-with-nodejs

    Quote: "The easiest way to send an email is by using the html parameter."

    Read 2 Nov 2026.

  19. S19Resend: Embed inline imageshttps://resend.com/docs/dashboard/emails/embed-inline-images

    Quote: "Inline images may be rejected by some clients (especially webmail)."

    Read 2 Nov 2026. Also: the content ID "must be less than 128 characters". The send-email reference (https://resend.com/docs/api-reference/emails/send-email) caps attachments at "max 40MB per email, after Base64 encoding".

  20. S20itsourcecode: How to Insert an Image in Gmail (third party, not Google)https://itsourcecode.com/blogs/how-to-put-or-insert-an-image-in-email-body-in-gmail/

    Quote: "Choose one of the tabs: Photos, Album, Upload, or Web address (URL)."

    Read 2 Nov 2026. Dated 14 Jul 2026. The Google help pages we read did not describe this option.

  21. S21Vercel Docs: URL_TOO_LONGhttps://vercel.com/docs/errors/URL_TOO_LONG

    Quote: "exceeds the maximum length allowed by the CDN (14 KB)"

    Read 2 Nov 2026. The error is HTTP 414. ChartPNG runs on Vercel. The page shows no date.