Table-Based Email Layout: Why It Persists in 2026 and How to Nest Tables Correctly
Build a marketing email with the layout tools you'd use for a website — flexbox, CSS grid, a few div columns — and it will look fine in your browser and then fall apart in Outlook. That's not a bug in your CSS; it's the inbox. Desktop Outlook on Windows still renders mail through Microsoft's Word engine, which ignores flexbox and grid, so the dependable way to control structure across email clients in 2026 is the same one developers have leaned on for two decades: nested HTML tables. Not because tables are elegant, but because they are the lowest common denominator every mail client understands. The catch is that "use tables" is only the headline. The reliable result lives in the details — `role="presentation"` so screen readers don't announce a layout grid as data, defining width in two places so Outlook and modern clients agree, and resetting `cellpadding`/`cellspacing`/`border-collapse` so cells don't inherit phantom gaps. This guide explains why tables persist, how to nest them without breaking, the small rules that actually matter, and how an editor can emit the whole bulletproof structure so you never hand-write a `<td>` again.
Mount it
// Design on a modern canvas; export bulletproof nested-table email HTML.
// BuilderJS is a DOM-mount bundle — no npm package, no React component.
const builder = new Builder({
mainContainer: '#bjs-main',
widgetsContainer: '#bjs-widgets',
settingsContainer: '#bjs-settings',
saveUrl: '/api/templates', // your own endpoint — BuilderJS never persists
loadAssets: 'auto',
});
// When the user is done, get the table-based email HTML + lossless JSON state.
async function exportEmail() {
const emailHtml = builder.getHtml(); // flex/grid → nested role=presentation tables,
// define-width-twice, cellpadding/cellspacing
// reset, MSO ghost tables, @media stacking
const designJson = builder.getData(); // persist this as your source of truth
// BuilderJS builds the markup — YOU own the send.
await fetch('/api/save-template', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: emailHtml, data: designJson }),
});
// ...then hand emailHtml to your ESP / SMTP service to actually deliver it.
}
Why email still uses tables in 2026
Email clients are not browsers. They run untrusted HTML from strangers, so they sanitize hard and render with engines that have nothing to do with Chrome or Safari. The headline case is Microsoft: desktop Outlook on Windows renders mail through the Word HTML engine, which has no support for flexbox or CSS grid and unreliable support for modern positioning, so any layout built on those will collapse there (as of June 2026; Microsoft has signaled a move away from the Word-based desktop Outlook around late 2026, but the installed base remains — verify on Microsoft's own pages). Tables are the fallback that works everywhere because every mail client, old and new, understands a `<table>` with rows and cells. That's the whole reason the technique refuses to die: it is the one layout primitive with near-universal support across the fragmented client landscape — Gmail, Outlook, Apple Mail, Yahoo, and the long tail of mobile and webmail apps. So in 2026 a structurally sound email is still a stack of tables, and the modern-CSS niceties (media queries, dark mode) are progressive enhancement layered on top — never the load-bearing structure.
The wrapper and container structure
A robust email isn't one table; it's a small set of nested ones with distinct jobs. The outermost is the full-width wrapper — a table at `width:100%` whose only purpose is to give you a paint surface that fills the client's viewport and lets you center the content. Inside it sits the container (or body) table, constrained to a fixed content width — commonly 600px, which fits comfortably in most preview panes. Inside the container go your content rows, and any multi-column section becomes its own nested table whose `<td>` cells are the columns. The pattern is wrapper → container → rows → column tables, nesting deeper only where the design needs columns. Keep nesting shallow and deliberate: every extra level is more markup to bloat the file and more chances for a client to mis-render. The mental model is plumbing, not painting — outer table positions and centers, inner tables hold the actual blocks, and columns are cells, not floated divs.
The four rules that make nesting bulletproof
Four details separate a table layout that holds together from one that leaks. First, role=presentation: add `role="presentation"` to every layout table so assistive technology treats it as visual scaffolding, not a data grid it should read row-by-row — without it, screen readers announce 'table, 3 columns' over your hero image. Second, define width twice: set both the HTML attribute (`width="600"`) and the inline CSS (`style="width:600px"`) on fixed-width tables and cells, because Outlook's Word engine trusts the attribute while modern clients honor the CSS — give them each what they read and they agree. Third, reset cellpadding/cellspacing: put `cellpadding="0" cellspacing="0"` on the tag and `border-collapse:collapse` in the style, or clients add their own default gaps between cells and you fight invisible spacing forever; control padding deliberately on each `<td>` instead. Fourth, suppress Outlook's phantom spacing with `mso-table-lspace:0pt; mso-table-rspace:0pt` and `table-layout:fixed` so columns don't drift. Get these four right and the same markup renders consistently from Outlook 2016 to Apple Mail.
Design in a normal canvas — the builder emits the tables
Hand-writing nested tables with all four rules, on every section, for every email, is exactly the tedious, error-prone work you want a tool to own. This is where BuilderJS fits. It is a drag-and-drop builder library you embed inside your own app, where your users design on an ordinary modern canvas — flexbox-style rows, columns, drag-and-drop blocks — without thinking about tables at all. When you export email, its EmailExportPipeline does the translation: it converts each flex/grid layout into a real nested `<table>`, stamps `role="presentation"`, sets `cellpadding="0" cellspacing="0"` with `border-collapse:collapse; table-layout:fixed`, writes the container width both as an attribute and inline (the define-width-twice rule), adds `mso-table-lspace`/`mso-table-rspace` resets, wraps the body in MSO conditional 'ghost table' comments for the Outlook Word renderer, injects an `@media (max-width:600px)` rule so columns stack on mobile, and gives every `<img>` `max-width:100%; height:auto` so images stay fluid. Nested grids are processed deepest-first so inner column tables nest correctly inside outer ones. You design in the comfortable canvas; the pipeline emits the bulletproof structure.
It builds the markup — you own the send and the page output
Keep the boundary clear: BuilderJS is the editor, not a sending platform. It produces the email HTML — nested tables, inlined CSS, MSO conditionals and all — and then your own stack takes over. You bring the ESP or SMTP service that actually delivers the message; BuilderJS never sends, hosts a list, schedules a campaign, or handles deliverability. The workflow is design, then `getHtml()` for the rendered table-based markup and `getData()` for the lossless JSON you persist, then hand the HTML to whatever sends your mail. The same editor also has a second output mode: for landing and web pages it emits clean, modern-CSS HTML — no tables, because real browsers handle flexbox and grid fine — so you don't run two editors or hand-rewrite a campaign into web markup. Pick the output that matches the destination: ruthless nested tables for the inbox, normal responsive HTML for the browser. Every email your users design runs through that one EmailExportPipeline, so the same transform proven on the 30+ bundled templates and 200+ samples — Litmus-tested across 22 email clients — is the transform applied to your output too. You can try the editor live at builder.emotsy.com.
Frequently asked questions
-
Why does HTML email still need tables in 2026 when websites don't?
Because email clients aren't browsers. Desktop Outlook on Windows renders mail through Microsoft's Word engine, which ignores flexbox and CSS grid, so layouts built on modern CSS collapse there (as of June 2026 — verify on Microsoft's pages). Nested tables are the one layout primitive with near-universal support across Gmail, Outlook, Apple Mail, Yahoo and the rest, so they remain the dependable structure. Modern CSS like media queries is layered on top as enhancement, not as the load-bearing layout.
-
What does role="presentation" do on an email table?
It tells assistive technology that the table is visual scaffolding, not a data table. Without it, a screen reader may announce your layout as a grid ('table, 3 columns') and read it cell by cell, which is confusing over a hero image or a button row. Adding role="presentation" to every layout table keeps the email accessible. BuilderJS's export pipeline stamps role="presentation" on every table it generates automatically.
-
Why do you define the width twice in email tables?
Because different clients read width from different places. Outlook's Word engine trusts the HTML width attribute (width="600"), while modern clients honor the inline CSS (style="width:600px"). Setting both means every client gets the value it reads and they agree on the layout. Omitting one is a common cause of an email that's the right width in Gmail but wrong in Outlook. The BuilderJS EmailExportPipeline writes both forms for you.
-
What's the point of cellpadding="0" cellspacing="0" and border-collapse?
They reset the default spacing clients add between and inside cells. Without cellpadding="0" cellspacing="0" on the tag and border-collapse:collapse in the style, you get phantom gaps you can't easily explain or remove. Resetting them gives you a clean slate, then you add padding deliberately on each <td> where you want it. BuilderJS applies these resets plus Outlook's mso-table-lspace/mso-table-rspace:0pt on every exported table.
-
Does BuilderJS make me build the nested tables by hand?
No — that's the whole point. You design on a normal drag-and-drop canvas using flex-style rows and columns, and the EmailExportPipeline converts that into nested role="presentation" tables on export: define-width-twice, cellpadding/cellspacing reset, MSO ghost tables, mobile @media stacking and fluid images included. You never hand-write a <td>. BuilderJS builds the markup; it doesn't send the email — you connect the exported HTML to your own ESP or SMTP service.