| name | email-template-builder |
| description | Build production-grade email template systems with React Email or MJML. Covers responsive layouts, dark mode, multi-provider sending (Resend, SendGrid, Postmark, SES), i18n, spam score optimization, preview servers, and analytics tracking. Use when setting up transactional email infrastructure, building email design systems, or debugging deliverability issues.
|
| license | MIT + Commons Clause |
| metadata | {"version":"1.0.0","author":"borghei","category":"marketing","domain":"email-engineering","tier":"POWERFUL","updated":"2026-03-09T00:00:00.000Z","frameworks":"react-email, mjml, email-deliverability, responsive-email"} |
Email Template Builder
Tier: POWERFUL
Category: Engineering / Marketing
Tags: email templates, React Email, MJML, responsive email, deliverability, transactional email, dark mode
Overview
Build complete transactional email systems: component-based templates with React Email or MJML, multi-provider sending abstraction, local preview with hot reload, i18n support, dark mode, spam optimization, and UTM tracking. Outputs production-ready code for any major email provider.
This skill builds the email rendering and sending infrastructure. For writing email copy and designing sequences, use email-sequence.
Architecture Decision: React Email vs MJML
| Factor | React Email | MJML |
|---|
| Component reuse | Full React component model | Partial (mj-attributes) |
| TypeScript | Native | Requires build step |
| Preview server | Built-in (email dev) | Requires separate setup |
| Email client compatibility | Good (renders to tables) | Excellent (battle-tested) |
| Dark mode | CSS media queries | CSS media queries |
| Learning curve | Low (if you know React) | Low (HTML-like syntax) |
| Best for | Teams already using React | Maximum email client compat |
Recommendation: React Email for TypeScript teams shipping SaaS. MJML for marketing teams needing maximum compatibility across Outlook, Gmail, Apple Mail, and legacy clients.
Project Structure
emails/
├── components/
│ ├── layout/
│ │ ├── base-layout.tsx # Shared wrapper: header, footer, styles
│ │ ├── button.tsx # CTA button component
│ │ └── divider.tsx # Styled horizontal rule
│ ├── blocks/
│ │ ├── hero.tsx # Hero section with heading + text
│ │ ├── feature-row.tsx # Icon + text feature highlight
│ │ ├── testimonial.tsx # Quote + attribution
│ │ └── pricing-table.tsx # Plan comparison
├── templates/
│ ├── welcome.tsx # Welcome / confirm email
│ ├── password-reset.tsx # Password reset link
│ ├── invoice.tsx # Payment receipt / invoice
│ ├── trial-expiring.tsx # Trial expiration warning
│ ├── weekly-digest.tsx # Activity summary
│ └── team-invite.tsx # Team invitation
├── lib/
│ ├── send.ts # Unified send function
│ ├── providers/
│ │ ├── resend.ts # Resend adapter
│ │ ├── sendgrid.ts # SendGrid adapter
│ │ ├── postmark.ts # Postmark adapter
│ │ └── ses.ts # AWS SES adapter
│ ├── tracking.ts # UTM parameter injection
│ └── render.ts # Template rendering
├── i18n/
│ ├── en.ts # English strings
│ ├── de.ts # German strings
│ └── types.ts # Typed translation keys
└── package.json
Base Layout Component
import {
Body, Container, Head, Html, Img, Preview,
Section, Text, Hr, Font
} from "@react-email/components";
interface BaseLayoutProps {
preview: string;
locale?: string;
children: React.ReactNode;
}
export function BaseLayout({ preview, locale = "en", children }: BaseLayoutProps) {
return (
<Html lang={locale}>
<Head>
<Font
fontFamily="Inter"
fallbackFontFamily="Arial"
webFont={{
url: "https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2",
format: "woff2",
}}
=
=
/>
{preview}
{children}
[Company] Inc. - [Address]
Unsubscribe
{" | "}
Email Preferences
{" | "}
Privacy Policy
);
}
body = { : , : , : , : };
container = { : , : , : , : , : };
header = { : , : };
content = { : };
divider = { : , : };
footer = { : };
footerText = { : , : , : , : , : };
link = { : , : };
Template Examples
Welcome Email
import { Button, Heading, Text } from "@react-email/components";
import { BaseLayout } from "../components/layout/base-layout";
interface WelcomeProps {
name: string;
confirmUrl: string;
trialDays?: number;
}
export default function Welcome({ name, confirmUrl, trialDays = 14 }: WelcomeProps) {
return (
<BaseLayout preview={`Welcome, ${name}! Confirm your email to get started.`}>
<Heading className="email-heading" style={h1}>
Welcome to [Product], {name}
</Heading>
<Text className="email-text" style={text}>
You have {trialDays} days to explore everything -- no credit card required.
Confirm your email to activate your account:
Confirm Email Address
Button not working? Paste this link in your browser:{" "}
{confirmUrl}
);
}
h1 = { : , : , : , : , : };
text = { : , : , : , : };
button = { : , : , : , : , : , : , : , : };
muted = { : , : , : , : };
linkStyle = { : , : };
Invoice Email
import { Row, Column, Section, Heading, Text, Hr, Button } from "@react-email/components";
import { BaseLayout } from "../components/layout/base-layout";
interface LineItem { description: string; amount: number; }
interface InvoiceProps {
name: string;
invoiceNumber: string;
date: string;
dueDate: string;
items: LineItem[];
total: number;
currency?: string;
downloadUrl: string;
}
export default function Invoice({
name, invoiceNumber, date, dueDate, items,
total, currency = "USD", downloadUrl,
}: InvoiceProps) {
const fmt = new Intl.NumberFormat("en-US", { style: , currency });
(
);
}
h1 = { : , : , : , : };
text = { : , : , : , : };
metaBox = { : , : , : , : };
metaLabel = { : , : , : , : , : , : };
metaValue = { : , : , : };
metaValueBold = { : , : , : , : };
rowEven = { : };
rowOdd = { : };
cell = { : , : , : };
cellRight = { ...cell, : };
divider = { : , : };
totalLabel = { : , : , : , : };
totalValue = { ...totalLabel, : };
button = { : , : , : , : , : , : , : , : , : };
Multi-Provider Send Abstraction
import { render } from "@react-email/render";
interface EmailPayload {
to: string;
subject: string;
template: React.ReactElement;
tags?: Record<string, string>;
}
interface EmailProvider {
send(payload: { to: string; subject: string; html: string; text: string; tags?: Record<string, string> }): Promise<{ id: string }>;
}
function getProvider(): EmailProvider {
const provider = process.env.EMAIL_PROVIDER || "resend";
switch (provider) {
case "resend": return require("./providers/resend").default;
case : ().;
: ().;
: ().;
: ();
}
}
() {
html = ((payload.), { : payload.?. || });
text = (payload., { : });
().({
: payload.,
: payload.,
html,
text,
: payload.,
});
}
UTM Tracking Injection
interface TrackingConfig {
campaign: string;
source?: string;
medium?: string;
}
export function addTracking(html: string, config: TrackingConfig): string {
const params = new URLSearchParams({
utm_source: config.source || "email",
utm_medium: config.medium || "transactional",
utm_campaign: config.campaign,
}).toString();
return html.replace(
/href="(https?:\/\/(?:www\.)?yourdomain\.com[^"]*?)"/g,
(match, url) => {
const sep = url.includes("?") ? "&" : "?";
return `href="${url}${sep}${params}"`;
}
);
}
i18n System
export interface EmailStrings {
welcome: {
preview: (name: string) => string;
heading: (name: string) => string;
body: (days: number) => string;
cta: string;
fallbackLink: string;
};
invoice: {
preview: (number: string, amount: string) => string;
heading: (number: string) => string;
greeting: (name: string) => string;
downloadCta: string;
};
common: {
unsubscribe: string;
preferences: string;
privacy: string;
};
}
{ } ;
: = {
: {
: ,
: ,
: ,
: ,
: ,
},
};
{ } ;
: = {
: {
: ,
: ,
: ,
: ,
: ,
},
};
Deliverability Checklist
DNS Records (Required)
Content Rules
Infrastructure
Email Client Compatibility
Known Quirks
| Client | Quirk | Workaround |
|---|
| Outlook (Windows) | No CSS grid/flexbox, ignores margin on images | Use <table> layout (React Email handles this) |
| Gmail | Strips <head> styles, limits CSS | Inline all styles (React Email handles this) |
| Apple Mail | Best support, renders dark mode well | Standard approach works |
| Yahoo Mail | Limited CSS support | Avoid advanced selectors |
| Outlook.com | Strips background images | Use background-color as fallback |
Testing Matrix
Test every template on these clients before production:
| Priority | Client | Method |
|---|
| Critical | Gmail (web) | Send test email |
| Critical | Apple Mail (iOS) | Send test email |
| Critical | Outlook (Windows, latest) | Litmus or Email on Acid |
| High | Outlook.com (web) | Send test email |
| High | Gmail (Android) | Send test email |
| Medium | Yahoo Mail | Litmus |
| Medium | Outlook (Mac) | Send test email |
Dev Workflow
npx email dev --dir emails/templates --port 3001
npx email export --dir emails/templates --outDir emails/dist
npx tsx emails/lib/send-test.ts --template welcome --to test@example.com
npx email lint --dir emails/templates
Common Pitfalls
| Pitfall | Consequence | Prevention |
|---|
| Using CSS grid/flexbox | Layout breaks in Outlook | Use Row/Column from React Email (renders to tables) |
| Container wider than 600px | Breaks on Gmail mobile | Max-width: 600px on container |
| Missing plain text version | Lower deliverability score | Always generate plain text with render(template, { plainText: true }) |
| Same domain for transactional + marketing | Marketing complaints tank transactional delivery | Separate sending domains/subdomains |
| Skipping email warm-up | Emails go to spam | Start low, increase gradually over 2-4 weeks |
| Dark mode ignoring | Unreadable emails for 30%+ of users | Add prefers-color-scheme: dark media queries with !important |
Related Skills
| Skill | Use When |
|---|
| email-sequence | Writing email copy and designing automation flows |
| analytics-tracking | Setting up email engagement tracking and attribution |
| launch-strategy | Coordinating email templates for product launches |
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|
| Email clipped in Gmail | HTML over 102KB | Run render_size_analyzer.py. Remove comments, minify, replace base64 images. |
| Layout broken in Outlook | CSS flexbox/grid used | Use table-based layout. Run template_validator.py for compatibility check. |
| Styles stripped in Gmail | Styles in <head> only | Inline all CSS. React Email handles this automatically. |
| Unreadable in dark mode | No dark mode CSS | Add prefers-color-scheme: dark media queries with !important. |
| Low deliverability score | Missing unsubscribe, heavy images | Run spam_score_checker.py. Add RFC 8058 one-click unsubscribe headers. |
| Images not loading | Blocked by email client defaults | Add descriptive alt text. Maintain 60%+ text-to-image ratio. |
| Template renders differently across clients | Unsupported CSS properties | Test on Gmail, Apple Mail, Outlook (Windows) before production sends. |
Success Criteria
- Spam score of 9+/10 on mail-tester.com before production sends
- Template renders correctly on Gmail, Apple Mail, and Outlook (Windows)
- HTML under 80KB (well under Gmail's 102KB clip threshold)
- Text-to-image ratio above 60%
- Dark mode tested and readable for 30%+ of users
- All images have alt text and explicit width/height dimensions
- One-click unsubscribe (RFC 8058) implemented in all templates
- Separate sending domains for transactional vs. marketing email
Scope & Limitations
In Scope: Email HTML/CSS template engineering, React Email and MJML components, multi-provider sending abstraction, i18n, dark mode, deliverability infrastructure, spam score optimization.
Out of Scope: Email copy/sequence writing (use email-sequence), marketing automation workflows, email list management, A/B test statistical analysis.
Python Automation Tools
1. Spam Score Checker (scripts/spam_score_checker.py)
Analyzes email HTML for spam risk: text-to-image ratio, link density, spam words, unsubscribe presence, HTML structure.
python scripts/spam_score_checker.py template.html
python scripts/spam_score_checker.py template.html --json
2. Template Validator (scripts/template_validator.py)
Validates email templates for client compatibility (Outlook, Gmail), accessibility, responsive design, and inline styles.
python scripts/template_validator.py template.html
python scripts/template_validator.py template.html --json
3. Render Size Analyzer (scripts/render_size_analyzer.py)
Analyzes template file size, estimates render weight, and checks against Gmail's 102KB clip threshold with detailed breakdown.
python scripts/render_size_analyzer.py template.html
python scripts/render_size_analyzer.py --dir templates/ --json