BuilderJS

Images in HTML Email: Why They Behave Nothing Like the Web

An image that looks perfect on a web page can arrive in an inbox as a blank gray box, a stretched-off-screen banner, or a mysterious 4-pixel gap that nothing in your CSS explains. That is not a bug in your code — it is the reality of email. Inboxes are not browsers. They block images by default, ignore relative URLs, render extra space under images, and fall back to alt text far more often than the web ever does. So the same `<img>` markup that works on a landing page is quietly wrong for email. This guide walks through the five things that actually trip people up — blocked-by-default images and the alt-plus-background-color fallback, why image src must be an absolute CDN URL, why `display:block` kills the gap, why `border:0` matters for old Outlook, and how to size images fluidly so they survive narrow phone screens. Then it shows how a drag-and-drop builder emits this exact image markup for you, and where the hosting boundary sits: BuilderJS is the editor you embed to produce the HTML — you still bring your own upload endpoint and CDN to actually store and serve the files.

Mount it

// What BuilderJS actually writes onto images during email export.
  //
  // 1) Fluid-image defense (EmailExportPipeline, step 6) — applied to EVERY <img>:
  //      if (!img.style.maxWidth) img.style.maxWidth = '100%';
  //      if (!img.style.height)   img.style.height   = 'auto';
  //    → <img src="https://cdn.yoursite.com/hero.png" width="600"
  //           style="max-width:100%; height:auto;" alt="...">
  //
  // 2) SVG→PNG rasterize (step 4.5) — carries the SVG aria-label across as alt
  //    and applies the gap + linked-image-border fixes:
  //      <img src=".../icons-styled/{style}/{platform}.png" width="32" height="32"
  //           alt="Facebook"
  //           style="display:block;border:0;outline:none;text-decoration:none;">
  //
  // You own the host. BuilderJS only references images by absolute URL —
  // it never stores or serves them:
  const builder = new Builder({
    mainContainer:     '#bjs-main',
    widgetsContainer:  '#bjs-widgets',
    settingsContainer: '#bjs-settings',
    upload: { url: '/your/upload-endpoint', maxBytes: 10 * 1024 * 1024 },
    // your endpoint stores the file in YOUR object store / CDN and returns its absolute URL
  });
  // Persist JSON for later edits; export inlined email HTML to hand to your sender.
  const json = builder.getData();
  const html = builder.getHtml(); // email artefacts run through EmailExportPipeline

Blocked by default: design for the no-image state first

Most email clients do not download remote images until the recipient explicitly clicks "display images," and many never auto-load them at all. That means the first thing a large share of your audience sees is the state with no images: empty boxes where your hero, logo, and product shots should be. Two things rescue that experience, and both belong on every image. First, real alt text — a clear, human description in the `alt` attribute — so a blocked image still communicates ("Spring sale — 30% off boots" beats a silent gray rectangle). Second, a background color on the image's container or table cell, so a blocked image degrades to a branded block instead of a stark white void, and so any light alt text stays legible. The mental shift is the opposite of the web: assume images are off, make the email work in that state, and treat the loaded images as the enhancement on top. If a recipient must see an image for the email to make sense, that is a design risk — put the critical message in live text, not baked into a picture.

Absolute CDN URLs only — there is no relative path in an inbox

On the web, `src="/images/logo.png"` resolves against the page's domain. An email has no domain to resolve against — it is opened inside Gmail, Outlook, or Apple Mail, detached from any site root — so a relative path simply fails, every time. Every image `src` in an HTML email must be a fully-qualified absolute URL: `https://cdn.yoursite.com/images/logo.png`, served over HTTPS from a host that is publicly reachable without authentication. That host should be a CDN or object store you control, not your app server behind a login. This is exactly the hosting boundary to be honest about: BuilderJS is the editor — it builds the markup and references your uploaded images by URL, but it does not host files or run a CDN. The download ships reference PHP upload backends (SQLite/MySQL and S3-compatible patterns) you copy and own, so you wire your own upload endpoint and point image URLs at your own storage. For the storage half of this — uploading editor images straight to an object store and serving them from a CDN domain — see the companion guide on builder image upload to S3 (/guide/builder-image-upload-to-s3/).

display:block kills the gap; border:0 fixes old Outlook

The classic "why is there a 4px gap under my image" puzzle has one cause: an `<img>` is an inline element by default, and inline elements sit on a text baseline, leaving space below for descenders. Email cells are tight, so that phantom gap is visible and ugly — a thin sliver under every image, and seams in sliced layouts. The reliable fix is `style="display:block;"` on the image, which removes it from the text flow and drops the gap entirely (set `vertical-align:bottom` as a secondary defense if the image must stay inline). Separately, older desktop Outlook and a few legacy clients render a blue or colored border around images wrapped in links, so the long-standing rule is to add `border:0;outline:none;text-decoration:none;` to every linked image. These are not optional polish — they are the difference between a clean render and an email that looks broken to a chunk of your list. A good editor bakes both into its exported image markup so you never hand-add them.

Fluid sizing: survive the phone without media-query support

A 600px-wide image dropped into a 320px phone viewport will either overflow and trigger horizontal scroll or get crushed, depending on the client — and you cannot rely on `@media` queries to save you, because Gmail's web client ignores media queries entirely (as of June 2026; see Litmus, https://www.litmus.com/blog/understanding-gmail-and-css-part-1). The always-on safety net is fluid sizing applied inline to the image itself: `max-width:100%; height:auto;`. That single declaration caps the image at its container's width on small screens while preserving aspect ratio, and it works even when a hostile filter strips every `<style>` block in the head. Combine it with a `width` attribute (the desktop intended size) so clients that respect attributes still lay it out correctly, and you have an image that holds up from desktop Outlook down to a narrow phone. The rule of thumb: never let an email image's only width hint live in a head stylesheet — put `max-width:100%` directly on the element where nothing can take it away.

How BuilderJS emits gap-free, alt-tagged, fluid image markup

Doing all of the above by hand on every image is tedious and easy to get wrong on the next edit — which is exactly what a builder is for. BuilderJS is a drag-and-drop editor you embed in your own app to design HTML email and pages; it produces the markup, and you bring your own upload/CDN backend and your own sending path. When you export email, its EmailExportPipeline applies the image rules described above automatically: step 6 is a fluid-image defense that writes `max-width:100%; height:auto` inline onto every `<img>` so images never overflow their cell, even if all head CSS is stripped. When it rasterizes an SVG (for example a styled social icon) to a PNG for client compatibility, it carries the icon's accessible label across as the image's `alt` and applies `display:block;border:0;outline:none;text-decoration:none;` — the exact gap-and-border fixes covered above. The same pipeline converts flexbox layouts to nested tables, inlines CSS, adds `@media` mobile stacking, and emits MSO conditional comments for desktop Outlook. The shipped templates and 200+ samples are Litmus-tested across 22 email clients, so the image markup is checked against the same fussy inboxes that block, gap, and stretch. You design once, call `getData()` to persist the JSON, and export inlined email HTML to hand to whatever sends your mail — BuilderJS owns the markup; you own the hosting and the send.

Frequently asked questions

Explore