CSS inlining in email
Why Gmail can't be trusted to render your stylesheet, and how to fix it without maintaining unmaintainable HTML
The problem
Gmail's support for <style> blocks in the <head> is partial and conditional, not a flat "no." Desktop webmail caps it at 16KB. The iOS and Android apps only honor it for genuine Google accounts, not third-party accounts added to the Gmail app. Miss any of those conditions and the block is ignored outright: elements revert to the browser's default rendering, Times New Roman body text, no spacing, no brand colors.
Gmail is used by well over a billion people. You can't ship a template that only renders correctly for some fraction of them depending on account type and message size.
The fix is to move every CSS rule from the <style> block into style="" attributes on the specific HTML elements those rules target. This is called CSS inlining. The result works in Gmail every time, regardless of account type or message size. The problem is maintaining it.
Why it's hard
Writing email HTML with fully inlined styles by hand is the maintenance nightmare the name suggests. Change a brand color and you're hunting through dozens of style="" attributes.
Reuse a paragraph style in three templates and you update it in three places. Any abstraction CSS normally gives you - variables, inheritance, the cascade - disappears.
Some email clients do support <style> blocks (Apple Mail, Outlook.com, many mobile clients). So the practical approach is to write email HTML with a <style> block as the source of truth, then run a build step that automatically inlines all the rules into style="" attributes before sending. This is what every email service provider does under the hood.
The catch: not all CSS rules can be inlined. Media queries, for example, must stay in a <style> block - inlining them is meaningless. So the inlining step must preserve your @media rules while moving everything else.
The manual solution
What you write vs. what gets sent
Write clean CSS in a <style> block:
<!-- Before inlining: clean, readable CSS in a <style> block -->
<html>
<head>
<style>
.container { max-width: 600px; margin: 0 auto; }
h1 { font-family: Arial, sans-serif; font-size: 24px; color: #111; }
p { font-family: Arial, sans-serif; font-size: 16px; color: #444; line-height: 1.5; }
.btn {
display: inline-block;
padding: 12px 24px;
background: #6e7ef0;
color: #ffffff;
border-radius: 6px;
text-decoration: none;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to Acme</h1>
<p>Your account is ready. Click below to get started.</p>
<a href="https://acme.com/dashboard" class="btn">Go to dashboard</a>
</div>
</body>
</html>After running through an inliner, every rule gets moved to a style="" attribute on its matching element:
<!-- After inlining: all CSS moved into style attributes -->
<!-- The <style> block is gone. Gmail is happy. -->
<html>
<head></head>
<body>
<div style="max-width:600px; margin:0 auto;">
<h1 style="font-family:Arial,sans-serif; font-size:24px; color:#111;">
Welcome to Acme
</h1>
<p style="font-family:Arial,sans-serif; font-size:16px; color:#444; line-height:1.5;">
Your account is ready. Click below to get started.
</p>
<a href="https://acme.com/dashboard"
style="display:inline-block;
padding:12px 24px;
background:#6e7ef0;
color:#ffffff;
border-radius:6px;
text-decoration:none;">
Go to dashboard
</a>
</div>
</body>
</html>Using Juice in a Node.js build step
Juice is the most widely used CSS inliner for Node.js. It handles selector specificity correctly and preserves @media rules:
import juice from 'juice';
const html = `
<html>
<head>
<style>
p { font-family: Arial, sans-serif; color: #444; }
.highlight { background: #fffbcc; }
</style>
</head>
<body>
<p>Normal text</p>
<p class="highlight">Highlighted text</p>
</body>
</html>
`;
const inlined = juice(html);
// <p style="font-family:Arial,sans-serif; color:#444;">Normal text</p>
// <p class="highlight" style="font-family:Arial,sans-serif; color:#444; background:#fffbcc;">
// Highlighted text
// </p>For production use, pass these options:
import juice from 'juice';
// Options for production email use
const inlined = juice(html, {
// Preserve !important declarations
preserveImportant: true,
// Keep @media queries in a <style> block. This is Juice's default,
// shown here for clarity. It only takes effect because
// removeStyleTags is left at its own default (true), so everything
// else gets inlined and stripped, leaving just the @media rules
// behind.
preserveMediaQueries: true,
});What survives inlining
Standard typographic and box-model properties inline correctly:
/* Juice inlines these correctly (safe to use) */
.element {
font-family: sans-serif;
font-size: 16px;
font-weight: bold;
color: #333333;
background-color: #f5f5f5;
padding: 16px;
margin: 0;
width: 600px;
text-align: left;
vertical-align: top;
border: 1px solid #cccccc;
line-height: 1.5;
text-decoration: none;
display: inline-block;
}What must stay in <style>
These rules only work inside a stylesheet. Inlining them is meaningless; Juice's preserveMediaQueries option keeps them in a <style> block so they still apply in clients that support it:
/* These must stay in <style>; inlining them is meaningless */
@media only screen and (max-width: 480px) {
/* Mobile breakpoints only fire inside a <style> block */
.col { display: block !important; }
}
a:hover {
/* Pseudo-classes only work in <style> */
color: blue;
}
li:nth-child(2) {
/* Structural pseudo-classes aren't preserved either: Juice drops
them entirely rather than leaving them in <style>. Verify the
actual inlined output if you rely on one. */
color: red;
}Inlined but ignored by classic Outlook anyway
These properties inline fine. Gmail and Apple Mail respect them, but classic Outlook silently discards them. Accept the degraded appearance in Outlook, or use VML fallbacks for the cases where it matters, such as border-radius on a button (see Outlook and MSO rendering):
/* These inline fine but classic Outlook ignores them anyway */
.element {
border-radius: 6px; /* Outlook shows square corners */
background-image: url("bg.jpg"); /* Outlook ignores the CSS property outright; needs VML */
display: flex; /* Outlook doesn't support flexbox */
display: grid; /* Outlook doesn't support grid */
}Gotchas to watch for
- Specificity conflicts when mixing inline and class styles. If you have both an inline style and a class rule targeting the same property on the same element, the inline style wins (higher specificity). This can cause unexpected results after inlining if your template already has some inline styles. Juice's
preserveImportantoption helps, but the safest approach is to pick one: either write everything in the<style>block (and inline it all), or write everything inline from the start. - @media rules must stay in <style>, but Gmail strips them anyway. Inlining preserves your media queries in the
<style>block. But Gmail removes that block, so your mobile breakpoints never fire in Gmail. This is a known limitation. Design your emails to look acceptable without responsive breakpoints as a baseline. - Structural pseudo-classes get dropped, not preserved.
a:hoveris recognized as a pseudo-class and correctly left in<style>. A structural selector likeli:nth-child(2)is silently dropped instead: not inlined, and not kept in<style>either. If a rule depends on one, check the actual inlined output rather than assuming it survived. - Handlebars templates need inlining at render time, not build time. If your email uses template variables (
{{firstName}}), inline the CSS before substituting variables, or use a library that handles Handlebars syntax transparently. Inlining after substitution can break if variable values contain CSS-like strings.
Sources
- Juice (Automattic): CSS inliner for Node.js - the official docs for the option names and behavior used in the code samples above
- Can I email... <style> element - the per-client support table behind Gmail's partial, conditional support
- Can I email... border-radius
- Can I email... display: flex
- Can I email... background-image
- Litmus: Email Client Market Share
- MDN: CSS specificity
Every target=email render call runs the HTML through Juice automatically before returning it. You write templates with clean, maintainable CSS and receive fully inlined HTML ready to drop into any email provider. The inlining happens after Handlebars substitution, so variable values never interfere with CSS parsing.