Email Rendering Playground
The same HTML email reaches each reader in a different shape: Gmail drops styles it can't parse, Outlook Classic draws with Word. Below, what each client breaks and how to check yours, then a playground for the 5 ways apps display HTML email.
@emailens/engine 0.18.22 · data last changed 10 Nov 2026
The email in a sandboxed srcdoc frame: styles stay in, scripts never run.
- This email
- 2.4 KB
- <style> blocks
- 1
- Scripts or inline handlers
- 1
Paste your own email above and check it, or open the checker and paste it there: five clients, no account.
No signup · Five clients free, Outlook Classic included · All 21 with an account
Which clients need an account
Gmail Android, Gmail iOS, Outlook 365, Outlook iOS, Outlook Android, Outlook for Mac, Apple Mail, Yahoo Mail Android, Yahoo Mail iOS, Samsung Mail, Thunderbird, HEY Mail, Proton Mail, AOL Mail, Fastmail, Superhuman open with a free account.
The checker needs a computer screen.
Why the same email renders differently in each client
A broken email costs you the click. The button that vanishes in Outlook, the layout that collapses on a phone in Yahoo and the text that turns unreadable in Gmail's dark mode all reach real readers, and nothing tells you. Each email client runs its own sanitizer and renderer over your HTML, so each reader gets a different version. These are the problems that cost the most, client by client. Dated lines are from our own captures on the real client.
Gmail
- Media queries and classes stop working, though the email is small.
- Gmail keeps <style> only in the <head> and only up to 16KB. A block in the body, or past the limit, is removed. Gmail's apps also drop <style> for accounts that are not Google accounts, so inline the styles your layout needs.
- Every style in the block vanishes at once.
- A colour written in the space syntax, such as rgb(30 41 59 / 1), makes Gmail remove the whole <style> block. In a style attribute it drops every declaration on that element. Write rgb(30, 41, 59) with commas.
- Columns built with flexbox collapse to their content.
- The flex shorthand is dropped: an item set to flex: 0 0 34% lost its 34%. A width: 34% on the same item held. Size columns with width, or use a table. Measured 2026-10-28, Gmail web in Chrome
- Green or brand-coloured text turns dark and hard to read in dark mode.
- Gmail on Android flips the lightness of text below a set brightness, so mid-tone text gets darker: #16a34a becomes #008e37, which fails contrast. Our model of the rule matched the real app's contrast verdict on all 424 held-out colour pairs. Measured 2026-10-31, Gmail on Android
Outlook Classic on Windows
- Cards lose their padding and the layout runs together.
- Outlook Classic renders email with Word. Padding on a <div> or a <table> is dropped: put it on the <td>. Flexbox and grid do nothing, so columns need tables.
- Hidden content shows up.
- display: none hides a cell, a row and a span, but not a <div> or a <table>. Add mso-hide: all.
- A photo is drawn huge, or a rounded picture is a blank box.
- An image is sized by its width and height attributes only. Inside VML, a picture in a <v:fill> showed only its fallback colour on Outlook 2013, 2016 and 2019; <v:imagedata> drew it, rounded corners included. Measured 2026-10-20, Outlook 2007 to 2019
- A web font turns into Times New Roman.
- A font loaded with @font-face falls back to Times New Roman on Outlook 2007 to 2016. Name a web-safe font after it in the stack and set it in an Outlook-only rule.
Yahoo Mail and AOL
- Phone readers get the desktop layout.
- Yahoo drops a whole <style> block when an @media query lists conditions with commas. Write one condition per @media rule. Measured 2026-10-29, Yahoo Mail web
- Flex columns shrink until words break letter by letter.
- The flex shorthand is dropped. Size flex items with width and flex-shrink: 0, or use a table. Measured 2026-10-27, Yahoo Mail web
- Everything after one rule loses its styles.
- An attribute selector whose value holds a semicolon, such as [style*="width:100%;"], breaks the rest of that stylesheet in Yahoo and AOL.
Outlook.com, new Outlook and Outlook mobile
- The second half of the stylesheet does nothing.
- These clients discard every rule after two closing braces in a row, }}, which minified CSS produces at the end of a media query. Put a space or a newline between the braces.
New Outlook for Mac
- A grid falls back to one column.
- Plain track sizes work (1fr 1fr, 300px 300px). A value with repeat() or minmax() is dropped. The client also drops var(), calc(), gradients and inline SVG. Measured 2026-10-27, new Outlook for Mac
Proton Mail
- A card loses its shadow, gap or flex layout but keeps its colour.
- Proton drops flex, gap, box-shadow, transform and similar properties from a style attribute that also holds a property it keeps. It also sets line-height: 1.5 on every element, so nothing inherits your line-height. Measured 2026-11-04, Proton web, 53 cases
More symptoms and their fixes: email rendering bugs by client.
How to check an email before you send it
- Lint the HTML. The open-source CLI flags each construct a client drops,
in your terminal or CI:
npx @emailens/cli lint email.html. - Preview every client. The checker shows what each client does to your HTML in about 1 second. It is measured against more than 2,200 real-inbox screenshots, and more arrive every day. The method is on the accuracy page.
- Confirm on real devices. Before a big send, get screenshots from real Outlook on Windows and Mac, Apple Mail on Mac and iPad, Gmail and Yahoo on the web and Android, and Proton.
- Automate it. The email rendering API takes your HTML and returns each client's version, the issues and a score per client, for a build step or a service. Coding assistants call the same engine through the MCP server.
Showing email inside your own app: 5 ways, with code
The playground at the top of this page is for a different job: displaying an email in a web app, such as an inbox, an email platform's template list or a support tool. How you embed it decides whether its CSS leaks into your page and whether its scripts run.
Direct Light DOM Injection
The email HTML is injected directly into a standard container element (like a div) using dynamic markup insertion. This is the simplest strategy but carries substantial risks: email styles leak outwards and contaminate the parent application layout, while global dashboard CSS rules bleed inwards and corrupt the email preview.
// Paradigm 1: React Light DOM Renderer
export function LightDOMRenderer({ emailHtml }: { emailHtml: string }) {
return (
<div
className="email-light-dom-container"
dangerouslySetInnerHTML={{ __html: emailHtml }}
/>
);
} // Chrome Extension Scanner Fallback
// Scans the top-level body document (weighted with a penalty since it's not isolated)
const html = document.documentElement.outerHTML;
const score = calculateScore(html) - 50; Shadow DOM Encapsulation
The email is loaded inside a browser-isolated Shadow DOM root attached to a host container. This completely solves the style bleeding issue without spinning up separate heavy iframe window threads. However, script blocks can still run unless intercepted, and default web tools like document.querySelectorAll will not see the email markup unless they explicitly drill through open shadowRoot nodes.
// Paradigm 2: React Shadow DOM Renderer (Encapsulated)
export function ShadowDOMRenderer({ emailHtml }: { emailHtml: string }) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
// Attaching or recycling open shadow root
let shadow = containerRef.current.shadowRoot;
if (!shadow) {
shadow = containerRef.current.attachShadow({ mode: 'open' });
}
shadow.innerHTML = `
<style>
:host { display: block; width: 100%; min-height: 500px; }
</style>
${emailHtml}
`;
}, [emailHtml]);
return <div ref={containerRef} className="w-full" />;
} // Chrome Extension Shadow Root Scanner
function deepSearchShadowRoots(root: ParentNode | ShadowRoot): string[] {
const results: string[] = [];
const elements = root.querySelectorAll("*");
for (const el of Array.from(elements)) {
if (el.shadowRoot) {
results.push(el.shadowRoot.innerHTML);
results.push(...deepSearchShadowRoots(el.shadowRoot));
}
}
return results;
} Static CORS Iframe Preview
An iframe with a static src attribute loading from an external rendering API or storage URL. This provides maximum security and isolation since the third-party domain operates outside the main site origin. The downside is that browser security restricts content scripts from accessing the iframe's DOM directly. To read it anyway, the emailens extension's background worker fetches the frame's URL itself and reads the response.
<!-- Paradigm 3: Cross-Origin Iframe Template Preview -->
<iframe
src="https://preview.klaviyo.com/email/render/template_12345"
width="100%"
height="600px"
sandbox="allow-popups allow-same-origin"
className="border-none"
></iframe> // Chrome Extension cross-origin iframe scanner
// The page cannot read the frame, so the extension's background worker fetches its URL
try {
const innerDoc = iframe.contentDocument || iframe.contentWindow?.document;
if (!innerDoc) throw new Error("Blocked by CORS");
} catch {
// The page can't read it: hand the URL to the extension's background worker
chrome.runtime.sendMessage({
type: "FETCH_CROSS_ORIGIN",
url: iframe.src
}, (response) => {
const rawEmailHtml = response.html; // Fetched by the extension, not read from the frame
});
} Dynamic Document Stream Writer
An empty same-origin blank iframe is mounted in the viewport, and JavaScript synchronously writes the raw email HTML directly to its open document stream. It solves style bleeding and is fully readable by content scripts immediately. Developers must be careful to call doc.close() to finalize parsing and stop loading animations.
// Paradigm 4: React Dynamic Stream Iframe Writer
export function DynamicIframeRenderer({ emailHtml }: { emailHtml: string }) {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc) return;
// Direct synchronous document stream injection
doc.open();
doc.write(emailHtml);
doc.close();
}, [emailHtml]);
return (
<iframe
ref={iframeRef}
width="100%"
height="600px"
className="border-none"
title="Dynamic Preview"
/>
);
} // Chrome Extension Same-Origin Iframe Scanner
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (doc && doc.documentElement) {
const emailHtml = doc.documentElement.outerHTML;
const score = calculateScore(emailHtml, iframe);
} The srcdoc Attribute Iframe (Highly Recommended)
The modern standard. Email HTML is embedded inside the srcdoc attribute of an iframe. When paired with standard sandboxing parameters (e.g. sandbox="allow-same-origin"), this achieves perfect style encapsulation, blocks script execution, and loads completely offline. The emailens engine extracts the raw unescaped content directly from the iframe's DOM attribute, bypassing loading delays.
<!-- Paradigm 5: The Secure Offline srcdoc Iframe -->
<iframe
srcdoc="<!DOCTYPE html><html><body><h1>Safe Preview</h1></body></html>"
width="100%"
height="600px"
sandbox="allow-popups allow-same-origin"
className="border-none rounded-xl"
></iframe> // Chrome Extension direct srcdoc extractor
const srcdoc = iframe.getAttribute("srcdoc");
if (srcdoc && srcdoc.trim() !== "") {
// Instantly read raw escaped HTML without waiting for iframe DOM rendering threads
const emailHtml = srcdoc;
} Your next question
Other free tools: HTML Email Checker · CSS Support Lookup · CSS Inliner · Dark Mode Preview · Outlook Ghost Table Generator · Outlook VML Background Generator · Bulletproof Button Generator · Client Targeting Generator
Run this check on every email
The open-source CLI checks every template, free: npx @emailens/cli lint email.html
(CLI docs).
GitHub Action (Pro). Pro is $19 a month. The pre-send gate in CI, and the history behind every check. See the plans →
Questions
Why does my HTML email look different in every email client?
Each client runs its own sanitizer and its own renderer. Gmail rewrites your CSS and drops whole <style> blocks it can't parse, Outlook Classic on Windows draws email with Word, and webmail strips properties that could break its own page. The same HTML reaches each reader in a different shape.
Which email client causes the most rendering problems?
Outlook Classic on Windows. It renders with Word, so padding on divs and tables, flexbox and border-radius are dropped, and display:none does not hide a div or a table. Gmail is next: it keeps <style> only in the head, up to 16KB, and removes the whole block over one colour it can't read.
Is there an email rendering API?
Yes. POST your HTML to the Emailens API and it returns each client's version of the email, the issues it found and a score per client. The same engine runs in the MCP server for coding assistants and in the open-source CLI.
Why does my email look different inside my app than in the inbox?
Because your app's CSS and the email's CSS meet. In the Light DOM they leak into each other; a Shadow DOM or a sandboxed srcdoc iframe keeps them apart.
Which way is safest for showing user-supplied email in an app?
A sandboxed srcdoc iframe without allow-scripts. Styles stay inside and scripts never run.
Does this playground send my email anywhere?
No. It renders in your browser. Only 'Check your email, free' sends it, to the checker, which opens it in five clients.