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.
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.
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.
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.
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:
<col> width specifications and applying them directly to <td> elements as inline stylestable-layout: fixed on the table element so that column widths are respected regardless of contentOlder 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 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:
<base target="_blank"> tag into the iframe's head and add click handlers that prevent navigation within the iframe.sandbox attribute to disable scripts, forms, and popups. Only allow-same-origin and allow-popups (for links) are enabled.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.
If you are building a product that displays HTML emails, here is what we learned the hard way:
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.