Mastering Email-Ready HTML: Automated Style Inlining with Tailwind CSS
Photo by Markus Spiske on Unsplash
In the world of email development, ensuring consistent styling across various email clients is a significant challenge. Email clients are notoriously inconsistent in their CSS support. Many strip out <style>
tags or ignore external stylesheets. So ensuring consistent appearance across various email clients often requires inline CSS styles. This requirement can conflict with modern development practices, especially when using utility-first CSS frameworks like Tailwind.
Recently, while working on a project that required sending various emails to users, I faced this exact dilemma. We were using Tailwind for our main application, and I wanted to maintain this efficient workflow for our email templates as well. However, the need for inline styles in emails posed a significant hurdle.
I considered several approaches:
1. Creating email templates with inline styles from the start.
Pros: Guaranteed compatibility with email clients.
Cons: Time-consuming to write and maintain, prone to errors, and incompatible with Tailwind.
2. Using online converters to transform Tailwind-styled templates into inline styles. Then, save the HTML with inline styles as a separate file to be sent via email.
Pros: Allows use of familiar Tailwind syntax during development.
Cons: Requires keeping two versions of each template (original and converted).
3. Utilizing an npm package to convert templates to inline styles at runtime.
Pros: It’s time-efficient and allows me to maintain only one version of each email template using Tailwind.
Cons: I did come across some packages like tailwind-to-css or tw-to-css, but while they have their merits, I encountered the following issues:
❌tailwind-to-css
generates random class names in the result HTML
❌tw-to-css
requires calling a converter function for every Tailwind class usage.
After evaluating these options and finding existing tools lacking, I decided to implement a custom solution. In this article, I’ll guide you through the development of a TypeScript module that automates the process of inlining Tailwind classes for email-ready HTML.
I want to highlight that the code actually runs on the Node.js server, so I aimed to use the bare minimum from the frontend world necessary to make the logic work.
Actual implementation
The main logic is resided in makeStylesInline
function:
export const makeStylesInline: TMakeStylesInline = async (
templatePath,
data,
) => {
const templateSource = fs.readFileSync(templatePath, 'utf8');
const template = Handlebars.compile(templateSource);
const html = template(data);
return inlineStyles(html);
};
This function takes a template path and optional data, compiles the template with Handlebars, and processes the resulting HTML to inline styles.
Harnessing the Power of Tailwind CSS
A key component is the processTailwindCSS
function, which applies Tailwind CSS to our HTML:
const processTailwindCSS = async (html: string): Promise<string> => {
const tailwindConfig = {
content: [{ raw: html, extension: 'html' }],
corePlugins: {
preflight: false,
},
};
const result = await postcss([
tailwindcss(tailwindConfig),
autoprefixer,
]).process('@tailwind components; @tailwind utilities;', {
from: undefined,
});
return result.css;
};
This function sets up a minimal Tailwind configuration and uses PostCSS to generate the necessary CSS.
Simplifying and Optimizing CSS for Emails
Given the limited CSS support in email clients, the simplifyColors
function optimizes the generated CSS:
const simplifyColors = (css: string): string => {
const generalSimplifications = css
.replace(/rgb\(([^)]+)\) \/ var\(--tw-[^)]+\)/g, 'rgb($1)')
.replace(
/rgba\(([^,]+),([^,]+),([^,]+),var\(--tw-[^)]+\)\)/g,
'rgba($1,$2,$3,1)'
)
.replace(/var\(--tw-[^)]+\)/g, '1')
.replace(/--tw-[^:]+:[^;]+;/g, '');
return generalSimplifications.replaceAll(
/(rgba?\(\d+\s+\d+\s+\d+\s*\/.*\))/g,
(match) => rgbToHex(match)
);
};
This function removes Tailwind-specific CSS variables and converts RGB colors to hexadecimal for better email client support.
The Art of Style Inlining
For the actual inlining process, we use the juice library:
const inlineStyles = async (html: string): Promise<string> => {
const tailwindCss = await processTailwindCSS(html);
const simplifiedCss = simplifyColors(tailwindCss);
return juice(html, {
extraCss: simplifiedCss,
applyStyleTags: true,
removeStyleTags: true,
preserveMediaQueries: true,
preserveFontFaces: true,
preserveImportant: true,
inlinePseudoElements: true,
});
};
This function processes the HTML with Tailwind, simplifies the resulting CSS, and then uses juice
to inline the styles. It preserves important structures like media queries and font faces.
Handling Dynamic Content with Handlebars
The solution integrates Handlebars for template compilation, allowing for dynamic content insertion:
const templateSource = fs.readFileSync(templatePath, 'utf8');
const template = Handlebars.compile(templateSource);
const html = template(data);
This approach enables the use of placeholder values in email templates, enhancing flexibility and reusability.
Conclusion
This TypeScript code offers an automated solution for generating email-ready HTML with inlined styles. By leveraging Tailwind CSS, PostCSS, and careful CSS optimization, it significantly streamlines the email development process. The result is consistently styled, cross-client compatible email HTML, generated with minimal manual intervention.
For developers looking to improve their email development workflow, this solution provides a robust starting point. It combines the utility-first approach of Tailwind CSS with the practicality of automated style inlining, all while maintaining the flexibility needed for dynamic email content.
For the full source code and more detailed implementation, visit the GitHub repository.
It’s important to note that this is version 1 of the solution, and as such, it may not cover all possible use cases or have all desired features. There’s always room for improvement and expansion, and I welcome feedback from developers who try this solution in their projects.
Thanks for reading! If you have any question or need some clarification, feel free to reach out.
Stay updated with the latest JavaScript and software development news! Join my Telegram channel for more insights and discussions: TechSavvy: Frontend & Backend.
Subscribe to my newsletter
Read articles from Vardan Hakobyan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Vardan Hakobyan
Vardan Hakobyan
Hi, I'm a seasoned software engineer with a strong focus on JavaScript. I'm passionate about sharing my knowledge and experiences to help others learn something new and avoid the same pitfalls I've encountered along the way.