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
// emails/components/layout/base-layout.tsx
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",
}}
fontWeight={400}
fontStyle="normal"
/>
<style>{`
@media (prefers-color-scheme: dark) {
.email-body { background-color: #111827 !important; }
.email-container { background-color: #1f2937 !important; }
.email-text { color: #e5e7eb !important; }
.email-heading { color: #f9fafb !important; }
.email-muted { color: #9ca3af !important; }
}
@media only screen and (max-width: 600px) {
.email-container { width: 100% !important; padding: 16px !important; }
}
`}</style>
</Head>
<Preview>{preview}</Preview>
<Body className="email-body" style={body}>
<Container className="email-container" style={container}>
<Section style={header}>
<Img
src={`${process.env.ASSET_URL}/logo.png`}
width={120} height={36} alt="[Product]"
/>
</Section>
<Section style={content}>{children}</Section>
<Hr className="email-muted" style={divider} />
<Section style={footer}>
<Text className="email-muted" style={footerText}>
[Company] Inc. - [Address]
</Text>
<Text className="email-muted" style={footerText}>
<a href="{{unsubscribe_url}}" style={link}>Unsubscribe</a>
{" | "}
<a href="{{preferences_url}}" style={link}>Email Preferences</a>
{" | "}
<a href="{{privacy_url}}" style={link}>Privacy Policy</a>
</Text>
</Section>
</Container>
</Body>
</Html>
);
}
// Styles (inline for email client compatibility)
const body = { backgroundColor: "#f3f4f6", fontFamily: "Inter, Arial, sans-serif", margin: 0, padding: "40px 0" };
const container = { maxWidth: "600px", margin: "0 auto", backgroundColor: "#ffffff", borderRadius: "8px", overflow: "hidden" };
const header = { padding: "24px 32px", borderBottom: "1px solid #e5e7eb" };
const content = { padding: "32px" };
const divider = { borderColor: "#e5e7eb", margin: "0 32px" };
const footer = { padding: "24px 32px" };
const footerText = { fontSize: "12px", color: "#6b7280", textAlign: "center" as const, margin: "4px 0", lineHeight: "1.6" };
const link = { color: "#6b7280", textDecoration: "underline" };
Template Examples
Welcome Email
// emails/templates/welcome.tsx
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:
</Text>
<Button href={confirmUrl} style={button}>
Confirm Email Address
</Button>
<Text className="email-muted" style={muted}>
Button not working? Paste this link in your browser:{" "}
<a href={confirmUrl} style={linkStyle}>{confirmUrl}</a>
</Text>
</BaseLayout>
);
}
const h1 = { fontSize: "24px", fontWeight: "700", color: "#111827", margin: "0 0 16px", lineHeight: "1.3" };
const text = { fontSize: "16px", lineHeight: "1.6", color: "#374151", margin: "0 0 24px" };
const button = { backgroundColor: "#4f46e5", color: "#ffffff", borderRadius: "6px", fontSize: "16px", fontWeight: "600", padding: "12px 24px", textDecoration: "none", display: "inline-block" };
const muted = { fontSize: "13px", color: "#6b7280", marginTop: "24px", lineHeight: "1.5" };
const linkStyle = { color: "#4f46e5", wordBreak: "break-all" as const };
Invoice Email
// emails/templates/invoice.tsx
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; }
int