foldout

Responsive email without a framework

A responsive email doesn't need a framework — it needs a layout that is already correct before any media query runs. Here is the fluid-hybrid pattern, written out in plain HTML.

Why the media query isn’t the plan

The usual responsive recipe is one desktop layout plus a @media (max-width: 600px) block that rearranges it on phones. In email that plan has a hole: several clients never run the media query.

  • Gmail app on Android and iOS, with a non-Gmail account attached (the “GANGA” case) strips the whole <style> block. No media queries, no classes, nothing.
  • Some corporate webmail and a long tail of smaller clients do the same when they sanitise the HTML.
  • Desktop Outlook runs neither media queries nor max-width on a <div>.

So the email has to already look right with every <style> rule deleted. The media query becomes an improvement on top, not the thing holding the layout up. That inversion is the whole technique and it’s why no framework is needed — there is nothing to abstract.

The fluid-hybrid skeleton

Three ideas stacked: an outer table at 100% width, an inner table that is fluid but capped by max-width, and a conditional ghost table that gives Outlook the fixed 600 px it understands.

the wrapper — works with all CSS stripped
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f2f4f7">
<tr>
  <td align="center" style="padding:24px 12px">

    <!--[if mso]><table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0"><tr><td><![endif]-->

    <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"
           style="max-width:600px; background:#ffffff; border-radius:12px">
      <tr>
        <td style="padding:32px">Content goes here.</td>
      </tr>
    </table>

    <!--[if mso]></td></tr></table><![endif]-->

  </td>
</tr>
</table>

align="center" on the outer <td> is an attribute, not CSS — it survives stripping. The inner table is width="100%" with max-width:600px, so it fills a 360-px phone and stops at 600 on a desktop. Outlook ignores max-width entirely, which is exactly why it gets the ghost table: inside the conditional comment it sees a real 600-px table, everyone else sees nothing.

Columns that stack without a media query

The same inversion applies to columns. Instead of floating <td>s and un-floating them on small screens, make each column an inline-block div with a max-width, inside a ghost table for Outlook. Narrow viewport, the divs wrap; wide viewport, they sit side by side.

two columns, no media query
<!--[if mso]><table role="presentation" width="600"><tr><td width="300" valign="top"><![endif]-->
<div style="display:inline-block; width:100%; max-width:300px; vertical-align:top">
<table role="presentation" width="100%"><tr><td style="padding:16px">Left</td></tr></table>
</div>
<!--[if mso]></td><td width="300" valign="top"><![endif]-->
<div style="display:inline-block; width:100%; max-width:300px; vertical-align:top">
<table role="presentation" width="100%"><tr><td style="padding:16px">Right</td></tr></table>
</div>
<!--[if mso]></td></tr></table><![endif]-->

One trap: whitespace between inline-block elements renders as a space, which can push the second column onto its own line at exactly 600 px. Either set font-size:0 on the parent <td> and restore the size inside each column, or close the tags tight — </div><div — with no newline between them.

The media query, now optional

With the layout already correct, the media query is only for polish: bumping type on small screens, widening padding, hiding a decorative image.

progressive enhancement, not load-bearing
<style>
@media only screen and (max-width: 480px) {
  .h1   { font-size: 24px !important; line-height: 30px !important; }
  .pad  { padding: 20px !important; }
  .hide-sm { display: none !important; }
}
</style>

!important is not optional here — clients inject their own styles and inline styles beat classes without it. Keep every one of these rules cosmetic. If deleting the whole <style> block breaks the email, the layout is wrong, not the client.

Images and type

Two attributes carry the responsive weight on images.

fluid image
<img src="https://example.com/hero.png" alt="Issue 12 cover"
   width="600" height="300"
   style="width:100%; max-width:600px; height:auto; display:block; border:0" />
  • width and height as attributes. Outlook sizes from these and ignores the CSS; they also reserve space so the layout doesn’t jump while images load.
  • height:auto in the style. Without it the fluid width squashes the image on phones.
  • display:block. Kills the descender gap under images inside table cells.
  • Real alt text. Many clients block images by default — the alt text is what a lot of readers see first, so write it as copy, not as a filename.

One more thing before the image goes in: compress it first. A 600 px hero straight off a camera can be two megabytes. That weight is not part of the 102 KB Gmail clips at — that limit is on the HTML — but it is what the reader waits for on a train, and images are the one part of an email you cannot make them wait less for by writing better markup.

For type, set a base font-size of 16 px and a line-height in the inline style of every text cell; anything smaller reads badly on a phone and some clients auto-zoom to compensate, which breaks the layout in a more visible way. Tap targets want 44 px of height — a button cell with padding:14px 24px around 16-px text gets there.

Check list before sending

  • Delete the <style> block and reload. The email must still be readable and correctly sized. This is the single most useful test of the whole technique.
  • Look at it at 320 px. Not 375 — 320 is still out there, and it’s where fixed-width cells break out.
  • Check the ghost tables are balanced. An unclosed conditional comment shows nothing in Chrome and destroys the layout in Outlook only.
  • Keep the whole message under 102 KB. Gmail clips past that and the unsubscribe link is at the bottom, on the clipped side.
  • Test one real send, not just a browser preview. Sending tools rewrite HTML — some inline everything and drop the media query with it.

If you’d rather not build the skeleton every time, MJML compiles to exactly this pattern — ghost tables, fluid inner table, media query and all. The MJML basics guide covers the tags, and the MJML editor compiles in the browser if you want to see the output before installing anything.

Tell us what happened

Goes straight to a person. No account, no ticket number.

What is this about?

We keep the message and the page it came from. Nothing else — not your IP, not your browser. Privacy.