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.
The short version
- Send a PNG. Do not rely on JavaScript, inline SVG or Base 64 images S8 S3 S4.
- Render at
dpr=2and setwidthandheightto the on-screen size S13. - Write alt text that says the finding. Classic Outlook for Windows blocks pictures by default S12.
- Use an opaque background unless you have tested dark mode.
- Keep the image URL short. For big charts, POST from your server and attach or host the PNG yourself.
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.
| Feature | Gmail | Outlook | Apple Mail |
|---|---|---|---|
| PNG image S1 | Supported | Supported | Supported |
| SVG as a linked image S2 | Partial. 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 S3 | No, 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 S4 | No. | No on Windows (2019: partial). Yes on Outlook.com, iOS, Android and macOS. | Yes. |
| picture and srcset S7 | No. The tags are replaced. | No on Windows, Outlook.com, iOS and Android. Mixed on macOS. | Yes, partial on recent macOS. |
| prefers-color-scheme S5 | No. | 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:
- Gmail shows images automatically by default, and hides them if it thinks the message is suspicious S9.
- Classic Outlook for Windows (Microsoft 365, 2016 to 2024) blocks automatic picture downloads by default, until the reader allows them S12. We did not check Outlook.com, new Outlook or the mobile apps.
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:
- The message gets bigger, and Resend warns that inline images may be rejected by some clients, especially webmail S19.
- Can I Email reports that Apple Mail on macOS ignored the
widthattribute on CID images when it was tested in 2019 S6. That breaks the retina trick below, so the CID examples also set the size in CSS. - With a URL, your chart server must be up when the reader opens the mail. With CID, the image travels with the message.
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:
- Your server will see requests from Google's proxy, not from each reader. Do not count opens by counting image requests.
- Treat an image URL as permanent. If the data changes, change the URL. A chart URL that contains the config changes by itself when the config changes.
- Do not reuse one URL for "the latest chart". Readers can keep seeing an old image.
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=2w 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:
- Some Outlook versions show retina images at full size and ignore the attributes. Litmus suggests a
max-widthrule in the style attribute, with thewidthattribute and noheightS13. Can I Email lists thewidthattribute as buggy in Outlook for Windows 2007 to 2019 S6. - Do not use
srcsetorpictureto serve 1x and 2x files. Gmail does not support them S7. One 2x file is enough.
<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
Dark inbox, same PNG
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
Dark inbox, same PNG
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:
- Bad:
alt="chart"oralt="Signups bar chart". - Better:
alt="Signups per weekday: Mon 12, Tue 19, Wed 7. Tuesday is the peak."
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:
- Gmail clipping. Gmail clips messages over 102 KB and shows "View entire message". Litmus says the limit counts the code, not the images S15. A chart URL is code. A config with a few hundred data points can make the URL several KB, and each chart in the email repeats that.
- Attachment size. If you embed with CID, the PNG counts toward the provider's limit. Resend allows up to 40 MB per email after Base 64 encoding S19.
- URL length. A GET URL carries the whole config, percent-encoded, so it is about 1.5 times the size of the JSON. ChartPNG accepts configs up to 16 KB, but its host refuses any URL over 14 KB with HTTP 414 S21. In practice a GET config over about 8 to 9 KB fails before ChartPNG sees it. We found no published URL limit for Gmail, Outlook or Apple Mail. As a precaution we suggest POST above about 8,000 characters.
When a URL gets long, stop putting the config in the URL. Two options that work today:
- POST the config from your server, get the PNG back, and send it as a CID attachment (examples below).
- 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.pngChartPNG 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 &. 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&w=600&h=360&dpr=2&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=white2. 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 &.
const img = '<img src="' + chartUrl.replaceAll('&', '&') + '" 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:
- Open the plain chart URL from example 1 in your browser and check the image.
- In the Gmail compose window, choose Insert photo, then the Web address (URL) tab.
- Paste the same plain URL (with
&, not&) 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
- PNG, from an
httpsURL or a CID attachment. dpr=2, withwidthandheightat the on-screen size.- Opaque background, or a transparent one you tested on a dark inbox.
- Alt text with the finding, and the key number in the email text.
- Total HTML well under 102 KB, with long configs sent by POST.
- A test send to Gmail, Outlook and Apple Mail, with images blocked and allowed.
What we could not confirm
- Any published limit on the length of an image URL in Gmail, Outlook or Apple Mail. We found none. The 8,000 character point where we suggest POST is our own precaution, not a documented limit. The 14 KB limit in S21 is for requests to ChartPNG's host, not for email clients.
- Whether Outlook.com, new Outlook, Outlook for Mac and the Outlook mobile apps block pictures by default. S12 covers classic Outlook for Windows only and shows no date.
- Whether Gmail, Outlook and Apple Mail each ignore JavaScript. S8 states that email design should avoid it as a general rule and does not name these clients.
- How long Gmail’s image proxy keeps an image. Google does not document it in anything we read, and S11 is a 2013 observation.
- Whether the Gmail "Insert photo, Web address" option is still in your version of Gmail. Only a third-party guide (S20) describes it, and we did not test it in a Gmail account.
- How Apple Mail, Outlook and Gmail invert or recolor transparent PNGs in dark mode today. S14 describes the general risk, but we did not test each client.
- Compatibility data older than 2023 (PNG, embedded svg, Base 64, width attribute) may no longer match current client versions. The test dates are given in the sources.
Sources
All pages read on 2 Nov 2026. Quotes are exact and short. Web pages change, so check the date.
- 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.
- 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.
- 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.
- 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).
- 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.
- 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."
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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".
- 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".
- 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."
- 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.
- 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".
- 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.
- 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.