Building Modular Email Systems with MJML: 50+ Components
The Email Rendering Nightmare
HTML email development is stuck in 2005. You need tables for layout, inline styles for compatibility, and extensive testing across dozens of email clients. Outlook uses Word's rendering engine. Gmail strips style tags. Every client has quirks that break standard CSS.
MJML solves this by providing a markup language that compiles to battle-tested HTML email. You write clean, semantic markup and MJML generates the horrible-but-compatible table-based HTML that email clients expect. I built a component library of 50+ MJML modules that my team uses for all email production.
Why a Component Library?
Without a component system, every email is built from scratch. Designers reinvent layouts, developers rewrite responsive code, and consistency across campaigns is impossible. A component library gives you:
- Consistency: Every email uses the same spacing, typography, and colour palette
- Speed: Assembling emails from components takes minutes, not hours
- Quality: Components are tested once across all email clients
- Maintainability: Updating a brand colour changes it across all future emails
Component Architecture
I organized components into categories:
Layout Components
- Header: Logo, navigation links, preheader text
- Footer: Social links, unsubscribe, legal text, address
- Section wrappers: Full-width, contained, split-column, asymmetric
- Spacers: Standardized spacing units (8px, 16px, 24px, 32px, 48px)
Content Components
- Hero: Full-width image with overlay text, background colour variants
- Product card: Image, title, price, CTA button
- Feature block: Icon, heading, description
- Testimonial: Quote, attribution, optional photo
- CTA button: Primary, secondary, ghost variants in all brand colours
Example Component: Product Card
<!-- product-card.mjml -->
<mj-section padding="16px 0">
<mj-column width="50%">
<mj-image
src="{{product_image}}"
alt="{{product_name}}"
width="280px"
border-radius="4px"
/>
</mj-column>
<mj-column width="50%" vertical-align="middle">
<mj-text font-size="20px" font-weight="700" padding="0 16px">
{{product_name}}
</mj-text>
<mj-text font-size="14px" color="#666" padding="8px 16px">
{{product_description}}
</mj-text>
<mj-text font-size="18px" font-weight="700" padding="8px 16px">
{{product_price}}
</mj-text>
<mj-button
background-color="#4d94d4"
border-radius="4px"
font-size="14px"
href="{{product_url}}"
>
Shop Now
</mj-button>
</mj-column>
</mj-section>Each component uses template variables (the double curly braces) that get replaced during assembly.
The Build Process
I set up a Node.js build pipeline:
const mjml = require('mjml')
const fs = require('fs')
const path = require('path')
function buildEmail(templatePath, variables) {
let source = fs.readFileSync(templatePath, 'utf-8')
// Replace template variables
for (const [key, value] of Object.entries(variables)) {
source = source.replace(
new RegExp(`\\{\\{${key}\\}\\}`, 'g'),
value
)
}
const result = mjml(source, {
validationLevel: 'strict',
minify: true
})
if (result.errors.length > 0) {
console.error('MJML errors:', result.errors)
}
return result.html
}The strict validation level catches issues at build time rather than in the inbox. Minification reduces email size, which matters because some email clients truncate large messages.
Composing Emails from Components
An email template includes components using MJML's include feature:
<mjml>
<mj-head>
<mj-include path="./includes/head.mjml" />
</mj-head>
<mj-body background-color="#f4f4f4">
<mj-include path="./components/header.mjml" />
<mj-include path="./components/hero-banner.mjml" />
<mj-include path="./components/product-card.mjml" />
<mj-include path="./components/product-card.mjml" />
<mj-include path="./components/cta-button.mjml" />
<mj-include path="./components/footer.mjml" />
</mj-body>
</mjml>Building a new email is now an assembly task: pick the components you need, arrange them in order, fill in the variables, and build.
Testing Across Clients
I tested every component across these critical email clients:
- Outlook 2019 (Windows, the Word rendering engine)
- Apple Mail (macOS and iOS)
- Gmail (web and mobile)
- Yahoo Mail
- Samsung Mail (surprisingly popular)
MJML handles most cross-client issues, but I still encountered edge cases. Outlook ignores border-radius. Gmail clips emails over 102KB. Some Android clients do not support web fonts. These limitations shaped the component designs.
Results
The component library reduced email production time from a full day to about 2 hours. New team members could produce on-brand emails in their first week. Cross-client rendering issues dropped to near zero because components were pre-tested. If you produce more than a handful of emails per month, investing in a MJML component library pays for itself almost immediately.