Generating Inline Styles from Tailwind for Email
AI generated
</>
tw
Tailwind CSS · Email · Build Tooling · PostCSS
Generating Inline Styles from Tailwind
the build workflow for compatible HTML email

Tailwind CSS utility classes are ignored by most email clients because external stylesheets never arrive in the inbox. A build pipeline that compiles Tailwind and automatically writes the generated rules as inline styles into every HTML element solves this without extra manual work.

17 min read Tailwind CSS · juice · PostCSS · Node.js HTML email · Outlook · Gmail

1. Why email clients don't load external CSS

A browser loads a stylesheet through <link rel="stylesheet"> and applies the rules to the document, regardless of whether the classes come from Tailwind, a custom framework, or hand-written CSS. Email clients like Outlook, Gmail, or the iOS Mail app do not do this reliably. Some strip the entire <head> section, others ignore <style> blocks in the body, and still others only support a subset of CSS selectors. The result: a template built with Tailwind that looks perfect in the browser arrives in the inbox as unformatted plain text.

The only CSS mechanism that works reliably across virtually all email clients is the style attribute directly on the HTML element. Inline styles are not filtered out because they are part of the element itself and require no separate resource or selector parsing pass. Anyone building HTML email with Tailwind CSS therefore needs a way to turn the utility classes that are convenient during development into exactly these inline styles at the end, without translating classes into style attributes by hand.

2. Utility classes in the browser versus inline styles in the inbox

In the normal frontend workflow you write class="px-6 py-4 bg-sky-600 text-white rounded-lg" and the Tailwind compiler generates a stylesheet once with the matching CSS rules that are applied to any number of elements through the cascade. This mechanism is Tailwind's strength in the browser, but it becomes a weakness in email because the stylesheet itself never reaches the recipient. The class exists in the HTML, but the associated rule is completely missing as soon as the client doesn't pick up CSS from the head section.

The solution is a two-stage process: first, Tailwind compiles a full stylesheet from the classes used, exactly as usual. Then an inlining tool takes every single rule from that stylesheet and writes it directly into the style attribute of the matching element. In the end you have HTML where class="px-6 py-4 bg-sky-600" has become style="padding-left:1.5rem;padding-right:1.5rem;padding-top:1rem;padding-bottom:1rem;background-color:#0284c7;", while the original classes can remain in the source for maintainability.


/* Tailwind's compiled stylesheet before inlining — one rule per utility class */
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
.bg-sky-600 { background-color: #0284c7; }
.text-white { color: #ffffff; }
.rounded-lg { border-radius: 0.5rem; }

3. Tools overview: juice, PostCSS, and custom scripts

For automatically generating inline styles, the package juice has become the standard in the Node ecosystem. It takes an HTML document and a CSS stylesheet, computes the specificity of the matching rules for each element, and writes the result as a style attribute into the tree. For Tailwind projects that means: Tailwind generates the CSS as usual, juice then takes over the inlining step, without writing your own CSS parsing logic.

Alternatively, PostCSS plugins exist that hook the inlining step directly into the existing PostCSS pipeline where Tailwind already runs. This reduces the number of build steps because no separate Node call is needed after the CSS build. For smaller projects, a custom script of just a few lines that calls juice.inlineContent() is also enough. In any case, it's important that the inlining tool leaves media queries and pseudo classes like :hover untouched in a remaining <style> block, because these cannot be meaningfully represented inline.


# Install the tools needed for the Tailwind-to-inline pipeline
npm install --save-dev tailwindcss juice

# tailwind.config.js scans only the email templates directory
# so unrelated utility classes never end up in the compiled CSS

4. The build pipeline: compile Tailwind, then inline

The complete pipeline consists of three steps that fit into a single npm script. First, the Tailwind CLI compiles a stylesheet from a source file that scans only the classes used in the email templates. Second, a small Node script reads both the HTML template and the generated CSS. Third, this script calls juice and writes the result as a finished, inline-styled HTML file into an output folder that is used directly by the mail delivery system.

The trick behind this pipeline: during development you keep working comfortably with Tailwind classes in the template, including editor autocomplete and instant visual feedback in the browser. Only the build step produces the inline styles that email clients require. This cleanly separates the development experience from the delivery format, much like Tailwind already does between source code and compiled stylesheet in the normal web context.


{
  "scripts": {
    "build:email": "node build-email.js",
    "watch:email": "tailwindcss -i ./src/email.css -o ./dist/email.css --watch"
  },
  "devDependencies": {
    "tailwindcss": "^4.0.0",
    "juice": "^10.0.0"
  }
}

// build-email.js — compile Tailwind, then inline every rule into style attributes
const fs = require("fs");
const juice = require("juice");
const { execSync } = require("child_process");

// Step 1: compile Tailwind CSS scoped to the email templates
execSync("npx tailwindcss -i ./src/email.css -o ./dist/email.css --minify");

const html = fs.readFileSync("./src/order-confirmation.html", "utf8");
const css = fs.readFileSync("./dist/email.css", "utf8");

// Step 2 + 3: inline every compiled rule into a style attribute
const inlined = juice.inlineContent(html, css, {
  removeStyleTags: true,
  preserveMediaQueries: true,
  preservePseudos: true,
});

fs.writeFileSync("./dist/order-confirmation.html", inlined);
console.log("Inline styles generated: dist/order-confirmation.html");

5. Practical example: order confirmation from classes to inline styles

A concrete example makes the effect tangible. An order confirmation contains a header with a logo, a product table, and a call-to-action button. In the source template you write the button just normally with Tailwind classes: class="inline-block bg-sky-600 text-white font-bold py-3 px-6 rounded-lg". After the build step, exactly this spot has a complete style attribute with all computed declarations, while the class itself usually stays in the output HTML and only serves as a fallback in case a client does respect CSS classes after all.

For layout elements like the outer table, which in email is usually built as an HTML <table> instead of flexbox or grid for compatibility reasons, the pipeline generates inline styles for cell padding, background colors, and borders. This lets you carry over the utility pattern known from Tailwind on the web almost unchanged onto the table-based email structure, except that inline styles arrive in the inbox instead of an external stylesheet.


<!-- Source template: Tailwind utility classes for readability during development -->
<table class="w-full bg-white rounded-lg overflow-hidden" cellpadding="0" cellspacing="0">
  <tr>
    <td class="px-6 py-4 bg-slate-50 border-b border-slate-200">
      <a class="inline-block bg-sky-600 text-white font-bold py-3 px-6 rounded-lg"
         href="https://mironsoft.de/order/12345">View order</a>
    </td>
  </tr>
</table>

<!-- Output after the inline build step: style attributes only, class kept as fallback -->
<table style="width:100%;background-color:#ffffff;border-radius:0.5rem;" cellpadding="0" cellspacing="0">
  <tr>
    <td style="padding:1rem 1.5rem;background-color:#f8fafc;border-bottom:1px solid #e2e8f0;">
      <a style="display:inline-block;background-color:#0284c7;color:#ffffff;font-weight:700;padding:0.75rem 1.5rem;border-radius:0.5rem;"
         href="https://mironsoft.de/order/12345">View order</a>
    </td>
  </tr>
</table>

6. Limits of inlining: pseudo classes and media queries

Not every Tailwind rule can be meaningfully represented as an inline style. Pseudo classes like hover:bg-sky-700 refer to an interaction state that doesn't exist within the style attribute itself. Such rules have to remain in a leftover <style> block in the head section, hoping that the given client respects that block. Good inlining tools like juice detect this automatically through the preservePseudos option and cleanly separate rules that can be inlined from those that cannot.

The same applies to responsive utility classes like sm:px-8, which are based on media queries. An inline style cannot express a condition such as a screen width, so these rules necessarily stay in a <style> block. Since media queries work in modern clients like Apple Mail and partly Gmail but are unreliable in Outlook, an email template's responsive behavior should never rely exclusively on them. Fluid table layouts with percentage widths are more robust than pure media query dependency here.


/* These rules stay in the remaining style block after inlining */
@media (max-width: 480px) {
  .sm\:px-8 { padding-left: 1rem !important; padding-right: 1rem !important; }
}
.hover\:bg-sky-700:hover { background-color: #0369a1; }

7. Systematically testing generated inline styles

After the build step, it's worth a visual check of the generated HTML in several real clients, not just the browser. Outlook on Windows uses the Word rendering engine for display, which supports a significantly narrower CSS subset than WebKit-based clients. An inline style that looks correct in the browser can still break in Outlook if it relies on a CSS property Word doesn't know, such as border-radius or box-shadow. These properties are simply ignored by Outlook, but the rest of the layout stays intact.

For automated checks in the build process, a simple lint step is useful that scans the generated HTML for known problem cases, for example leftover class references to classes that no longer exist in the CSS, or deeply nested <div> structures that are supported worse in email than flat table structures. Such a check runs in seconds and catches many errors before the template even lands in a real inbox.

8. Maintainability: why Tailwind stays in the source

One could argue it would be simpler to develop directly with inline styles and skip the detour through Tailwind entirely. In practice the opposite is true: maintaining inline styles by hand means searching and replacing the same color or the same spacing in multiple places in the HTML every time the design changes. With Tailwind in the source, the single source of truth for colors, spacing, and typography stays in tailwind.config.js, while the build step handles the translation into inline styles.

This pays off especially when several email templates share the same color scheme, for example order confirmation, shipping notification, and invoice. If the brand color changes, one adjustment in the Tailwind theme is enough, and all templates automatically generate updated inline styles on the next build. Without this pipeline, every single HTML file would have to be searched manually, which becomes an error source as the number of templates grows, one that an automated pipeline rules out from the start.

9. Inlining tools compared directly

When choosing the right tool for generating inline styles from Tailwind classes, it's worth looking at the concrete differences between the common options.

Tool Integration Pseudo classes Best fit
juice (Node) Standalone build script after Tailwind Kept in a style block Default choice for Tailwind projects
PostCSS plugin Directly in existing PostCSS pipeline Kept in a style block Fewer build steps, more configuration
MJML with Tailwind classes Compiles components to table HTML Limited via mj-style When layout components matter more
Manual inlining No build step, fully manual Error prone on changes Only for one-off, very small emails

juice remains the most pragmatic choice for most Tailwind-based email projects because it fits into an existing Node toolchain without major rework and correctly leaves media queries and pseudo classes in a remaining style block. PostCSS plugins are interesting when a more complex PostCSS configuration already exists. MJML fits when not just styling but also the table structure itself should be automated.


# Run the full pipeline as part of the deployment build
npm run build:email
ls -la dist/*.html   # inspect the inline-styled output before sending

Mironsoft

Tailwind CSS, email templates, and build pipelines for Magento and Symfony

HTML emails that look right in every inbox?

We build pipelines that automatically turn Tailwind classes into compatible inline styles, test the result in real clients, and integrate the templates into your sending system.

Build pipeline

Compile Tailwind and generate inline styles automatically with juice

Client testing

Checking the generated templates in Outlook, Gmail, and Apple Mail

Integration

Connecting to mail delivery systems in Magento and Symfony projects

10. Summary

Generating inline styles from Tailwind instead of writing them by hand solves the fundamental compatibility problem of HTML email: external stylesheets don't reliably arrive in the inbox, but inline styles do. A build pipeline made of the Tailwind CLI and an inlining tool like juice handles the translation automatically, while development itself still happens with convenient utility classes. Media queries and pseudo classes stay in a separate style block, because they cannot be meaningfully represented inline.

The biggest win of this approach lies in maintainability across multiple templates. Instead of manually maintaining colors and spacing in every single HTML file, the Tailwind theme stays the single source of truth, and every template automatically generates current inline styles on build. Anyone who regularly adjusts HTML emails saves significant manual effort with this pipeline while also reducing the risk of inconsistent inline styles between different templates.

Teams that additionally generate PDF documents from the same codebase benefit from looking at the separate pipeline with Puppeteer or Dompdf, because many basic principles like table-based layout and the consistent separation of source code and output format carry over directly. This investment in a clean build pipeline pays off across formats, regardless of whether the end result is an email or a PDF document.


# Quick sanity check before every send: no leftover unresolved classes
grep -c "class=" dist/order-confirmation.html
grep -c "style=" dist/order-confirmation.html
# Exit non-zero in CI if the inline step produced no output at all
test -s dist/order-confirmation.html

Generating Inline Styles from Tailwind — Key Takeaways

Why inline at all

Email clients ignore external stylesheets and often style blocks too. Only inline styles in the style attribute are reliably compatible.

The pipeline

Tailwind compiles the CSS, juice writes every rule as an inline style into the matching HTML element.

Limits

Pseudo classes and media queries stay in a separate style block, because inline styles cannot express conditions.

Maintainability

The Tailwind theme stays the single source of truth, all templates update their inline styles automatically on the next build.

11. FAQ: Generating Inline Styles from Tailwind

1Why aren't Tailwind classes enough in email?
Many clients don't load external stylesheets and sometimes strip style blocks. Only inline styles in the style attribute are reliably respected.
2What exactly does juice do?
juice reads HTML and CSS, computes matching rules per element, and writes them as an inline style into the tree.
3Do Tailwind classes remain in the output?
Configurable. Often kept as a fallback while the inline styles handle the primary rendering.
4What happens to hover classes?
Pseudo classes can't be inlined and remain in a leftover style block.
5Do media queries still work after inlining?
Yes, if the tool leaves them untouched, since inline styles cannot express a condition such as a screen width.
6Why not develop directly with inline styles?
Manually maintained inline styles scatter the same values across many files. Tailwind stays the single source of truth.
7How do I test generated inline styles?
In several real clients, especially Outlook. A lint step in the build can additionally spot known problem patterns.
8Does Outlook support all inline styles?
No, the Word engine ignores properties like border-radius or box-shadow, even when correctly set.
9Does the pipeline integrate well?
Yes, either as a standalone Node script or as a PostCSS plugin inside an existing configuration.
10Is it worth it for just one template?
For one rarely changed template, manual inlining may suffice. For several templates, the pipeline pays off quickly.