ENGINEERING

The Email Rendering Saga: 6 Commits to Fix HTML Tables

Andres MuguiraFebruary 26, 20267 min read
EmailHTMLRenderingCSS
← Back to Blog
Summarize with AI

The Problem Nobody Warns You About

Displaying HTML emails inside a web application sounds trivial. You receive the HTML from the Gmail API, sanitize it for security, and render it inside a container. What could go wrong? As it turns out, everything involving tables. And since HTML email is essentially nothing but tables (because email clients still do not reliably support flexbox or grid in 2026), everything goes wrong.

This is the story of six commits that fixed HTML table rendering in SalesSheet's email view. Each commit solved one specific problem. Each problem was caused by the interaction between the email's intended styling and our application's CSS. Together, they form a cautionary tale about the collision between modern web development and the fossil record that is HTML email.

HTML email is where modern CSS goes to die. Every rule you take for granted — cascade, specificity, inheritance — behaves differently when your content was authored for Outlook 2007.
Before: HTML email with broken table layout — columns collapsed to 100% width, text overflowing, layout unrecognizable

Commit 1: Tailwind Prose vs. Table Widths

SalesSheet's email viewer used Tailwind's prose class for typography. The prose plugin provides beautiful default styles for headings, paragraphs, lists, and links. It also provides default styles for tables: table { width: 100% } and td, th { padding: 0.5rem 0.75rem }. These defaults are perfect for data tables in blog posts. They are catastrophic for HTML emails.

HTML emails use tables for layout, not for data. A typical marketing email uses nested tables with pixel-precise widths: a 600px outer table, with 200px and 400px inner tables for a sidebar-content layout. When our prose styles forced all tables to width: 100%, the carefully constructed 600px layout exploded to fill the entire email container. Sidebar columns that should have been 200px became 33% of the container. Content columns that should have been 400px became 67%. Images overflowed. The layout was unrecognizable.

The fix: remove the prose class from the email body container and apply typography styles manually, excluding any table-related rules. We created a custom .email-body class that inherits prose's text styling (font size, line height, paragraph spacing) but explicitly resets table, td, and th to all: revert, which restores the browser's default table rendering and allows the email's inline styles to take effect.

Commit 2: The Regex Sanitizer Disaster

Before rendering email HTML, we sanitize it to remove potentially dangerous content: script tags, event handler attributes, and external resource loading. Our initial sanitizer used regular expressions to strip these elements. This is the approach that every blog post about HTML sanitization warns you not to use, and now we understand why.

The regex for removing style tags was: /<style[^>]*>[\s\S]*?<\/style>/gi. This was supposed to be targeting inline script tags, but a copy-paste error meant it was also removing <style> tags. Every email that relied on a <style> block in the <head> for its layout lost all of those styles during sanitization. The email would render with only its inline styles, which for many emails meant losing responsive breakpoints, font definitions, and class-based layouts.

The fix had two parts. First, we replaced the regex sanitizer with DOMParser. Instead of trying to match HTML patterns with regular expressions (which is provably impossible for the general case), we parse the HTML into a proper DOM tree, walk the tree, and remove dangerous nodes. DOMParser correctly handles nested tags, malformed HTML, and edge cases that regex cannot.

Second, we explicitly preserved <style> tags in the sanitized output. Style tags in emails are not a security risk — they do not execute code. They need to be preserved for the email to render correctly.

Commit 3: Style Tag Scoping

Preserving style tags solved the rendering problem but created a new one: style pollution. An email's <style> block might contain rules like a { color: blue; } or p { font-size: 14px; }. When rendered inside our application, these rules leaked out of the email container and affected the surrounding UI. Links in the sidebar turned blue. Paragraph spacing in the navigation changed. The email's styles were overwriting our application's styles.

The fix was to scope all email styles to the email container. During sanitization, we parse each <style> tag's contents, prepend every CSS selector with .email-body-container, and write the scoped styles back. A rule like a { color: blue; } becomes .email-body-container a { color: blue; }. This ensures the email's styles only apply within the email viewer, not to the broader application.

The scoping logic handles most CSS constructs correctly: class selectors, ID selectors, element selectors, pseudo-classes, combinators, and media queries. We use a lightweight CSS parser rather than regex for this transformation, because CSS selector syntax is complex enough that regex matching breaks on edge cases like attribute selectors with brackets.

Commit 4: Google Sheets Compatibility

When a user pastes a table from Google Sheets into a Gmail message and sends it, the resulting HTML is uniquely terrible. Google Sheets generates table markup with inline styles that use a mix of absolute pixel widths, percentage widths, and no widths at all — sometimes within the same table. Column widths are specified on <col> elements that email clients often ignore. Cell content uses <span> elements with inline font-family and font-size that override any surrounding styles.

Our email viewer was rendering these tables as a mess. Columns were uneven. Some cells were impossibly narrow while others stretched to fill available space. The border styling (which Google Sheets applies via a separate <style> block with class-based selectors) was missing because our earlier style tag fixes had not yet been deployed when we first noticed this issue.

The fix targeted Google Sheets specifically. We detect Google Sheets tables by looking for the characteristic data-sheets-* attributes that Google injects. When detected, we normalize the table by:

Commit 5: The Width Attribute Fallback

Older HTML emails (and many email-building tools that prioritize Outlook compatibility) use the HTML width attribute on table elements rather than CSS width. For example: <table width="600"> instead of <table style="width:600px">. Modern browsers respect both, but our CSS reset was overriding the attribute-based widths.

The issue was in our email container's CSS. We had a rule: .email-body-container table { max-width: 100%; } intended to prevent emails from being wider than the container. This rule correctly limited tables with CSS widths. But for tables with only the HTML width attribute, the browser computed the attribute width as the element's width and then the CSS max-width clamped it to the container width. So far, so correct. But we also had box-sizing: border-box applied globally, which meant the table's padding and borders were subtracted from the max-width, making the table slightly narrower than intended. This cascaded through nested tables, each losing a few pixels, until the innermost table was noticeably narrower than designed.

The fix: apply box-sizing: content-box to all tables within the email container. This ensures that the width attribute value is treated as content width (as HTML table width has always been defined), and padding and borders are added on top. The outer max-width: 100% still prevents overflow, but the internal layout math now works correctly.

After: HTML email rendered correctly in a sandboxed iframe — 180px sidebar + 420px content, CSS cascade fully isolated

Commit 6: The Iframe Isolation

After five commits of increasingly specific CSS fixes, we stepped back and asked: is there a better architecture for this? The fundamental problem was that email HTML and application HTML live in the same document, sharing the same CSS cascade. Every fix was a patch to prevent one from affecting the other. Each patch had edge cases. The surface area for future bugs was enormous.

The answer was to render emails in an iframe with srcdoc. Instead of inserting sanitized HTML into our DOM, we render it in a sandboxed iframe that has its own document, its own CSS cascade, and its own completely isolated rendering context. The email's styles cannot leak out. Our application's styles cannot leak in. The email renders exactly as the sender intended, in its own little world.

The iframe approach required some additional work:

Sometimes the right fix is not a better patch. It is a better architecture. Five commits of CSS specificity battles taught us that email HTML does not belong in our DOM. One commit of iframe isolation made all five previous fixes unnecessary.
Architecture diagram: Raw HTML to DOMPurify sanitize to Scoped styles to Sandboxed iframe rendering

Lessons for Anyone Rendering HTML Email

If you are building a product that displays HTML emails, here is what we learned the hard way:

  1. Use iframe isolation from day one. Do not try to render email HTML in your document. The CSS cascade conflicts are endless and each fix creates new edge cases. An iframe with srcdoc gives you perfect isolation with minimal overhead.
  2. Never use regex to process HTML. Use DOMParser. It handles malformed HTML, nested tags, and edge cases that regex cannot. The performance difference is negligible.
  3. Test with real emails. Do not test with hand-written HTML. Test with actual emails from Gmail, Outlook, Apple Mail, Mailchimp, HubSpot, and Google Sheets pastes. Each one generates different HTML with different quirks.
  4. Email HTML is not web HTML. It uses tables for layout. It uses inline styles for everything. It uses HTML attributes that modern web development abandoned a decade ago. Treat it as a foreign format, not as content your application authored.

Six commits. Six different problems. One underlying cause: email HTML is wild, untamed content that fights against every assumption modern web development makes. Respect it, isolate it, and test it with real data. Your users send and receive thousands of emails. Every one of them needs to render correctly.

Try SalesSheet Free

No credit card required. Start selling smarter today.

Start Free Trial