October 2026 Workspaces: invite your team

CSS inlining in email

Why Gmail can't be trusted to render your stylesheet, and how to fix it without maintaining unmaintainable HTML

No fixed retirement date. The Outlook-specific workarounds on this page are tied to classic Outlook's Word rendering engine, which is not being replaced or patched. Microsoft is pushing a separate, browser-based new Outlook as the default starting April 2026, but classic Outlook, Word engine included, stays supported until at least 2029, with no cutover date announced beyond that. Why the October 2026 date you may have seen is wrong

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 preserveImportant option 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:hover is recognized as a pseudo-class and correctly left in <style>. A structural selector like li: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

How Postcraft handles this

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.

Try it free