Email HTML is notoriously difficult — every client renders differently. React Email gives you React components that compile to battle-tested HTML.
Install Dependencies
pnpm add @react-email/components react-email resend
Project Structure
emails/
WelcomeEmail.tsx
ReceiptEmail.tsx
DigestEmail.tsx
components/
EmailLayout.tsx
EmailButton.tsx
EmailFooter.tsx
Shared Email Layout
// emails/components/EmailLayout.tsx
import {
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from "@react-email/components";
import type { ReactNode } from "react";
interface EmailLayoutProps {
preview: string;
children: ReactNode;
}
export function EmailLayout({ preview, children }: EmailLayoutProps) {
return (
<Html>
<Head />
<Preview>{preview}</Preview>
<Tailwind>
<Body className="bg-gray-100 font-sans">
<Container className="mx-auto max-w-xl bg-white rounded-lg overflow-hidden my-8">
{/* Header */}
<Section className="bg-blue-600 px-8 py-6">
<Img
src="https://example.com/logo-white.png"
width={120}
height={36}
alt="Company Name"
/>
</Section>
{/* Content */}
<Section className="px-8 py-6">{children}</Section>
{/* Footer */}
<EmailFooter />
</Container>
</Body>
</Tailwind>
</Html>
);
}
Email Footer
// emails/components/EmailFooter.tsx
import { Hr, Link, Section, Text } from "@react-email/components";
export function EmailFooter() {
return (
<Section className="px-8 py-4 bg-gray-50">
<Hr className="border-gray-200" />
<Text className="text-xs text-gray-500 text-center mt-4">
Company Name Inc. · 123 Main St · City, ST 12345
</Text>
<Text className="text-xs text-gray-400 text-center">
<Link href="{{unsubscribe_url}}" className="text-gray-400 underline">
Unsubscribe
</Link>{" "}
·{" "}
<Link href="https://example.com/preferences" className="text-gray-400 underline">
Email Preferences
</Link>
</Text>
</Section>
);
}
Email Button Component
// emails/components/EmailButton.tsx
import { Button } from "@react-email/components";
interface EmailButtonProps {
href: string;
children: string;
variant?: "primary" | "secondary";
}
export function EmailButton({
href,
children,
variant = "primary",
}: EmailButtonProps) {
const styles =
variant === "primary"
? "bg-blue-600 text-white"
: "bg-white text-blue-600 border border-blue-600";
return (
<Button
href={href}
className={`rounded-md px-6 py-3 text-sm font-semibold no-underline text-center ${styles}`}
>
{children}
</Button>
);
}
Welcome Email
// emails/WelcomeEmail.tsx
import { Heading, Text, Section, Hr } from "@react-email/components";
import { EmailLayout } from "./components/EmailLayout";
import { EmailButton } from "./components/EmailButton";
interface WelcomeEmailProps {
name: string;
loginUrl: string;
}
export default function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
return (
<EmailLayout preview={`Welcome to our platform, ${name}`}>
<Heading className="text-2xl font-bold text-gray-900 mb-0">
Welcome, {name}
</Heading>
<Text className="text-gray-600 text-base leading-relaxed">
We are excited to have you on board. Your account is ready and you can
start using all our features right away.
</Text>
<Section className="text-center my-8">
<EmailButton href={loginUrl}>Get Started</EmailButton>
</Section>
<Hr className="border-gray-200" />
<Text className="text-sm text-gray-500">
Here is what you can do next:
</Text>
<ul className="text-sm text-gray-600 space-y-2">
<li>Complete your profile</li>
<li>Explore the dashboard</li>
<li>Invite your team</li>
</ul>
</EmailLayout>
);
}
// Preview props for development
WelcomeEmail.PreviewProps = {
name: "Alex",
loginUrl: "https://example.com/login",
};
Receipt Email
// emails/ReceiptEmail.tsx
import { Heading, Text, Section, Row, Column, Hr } from "@react-email/components";
import { EmailLayout } from "./components/EmailLayout";
interface LineItem {
name: string;
quantity: number;
price: number;
}
interface ReceiptEmailProps {
customerName: string;
orderNumber: string;
date: string;
items: LineItem[];
subtotal: number;
tax: number;
total: number;
}
function formatCurrency(amount: number) {
return `$${(amount / 100).toFixed(2)}`;
}
export default function ReceiptEmail({
customerName,
orderNumber,
date,
items,
subtotal,
tax,
total,
}: ReceiptEmailProps) {
return (
<EmailLayout preview={`Receipt for order ${orderNumber}`}>
<Heading className="text-2xl font-bold text-gray-900 mb-0">
Payment Receipt
</Heading>
<Text className="text-gray-600 text-sm">
Hi {customerName}, here is your receipt for order #{orderNumber} placed
on {date}.
</Text>
{/* Line Items */}
<Section className="mt-6">
{/* Table Header */}
<Row className="border-b border-gray-200 pb-2">
<Column className="w-[50%]">
<Text className="text-xs font-semibold text-gray-500 uppercase m-0">
Item
</Text>
</Column>
<Column className="w-[20%] text-center">
<Text className="text-xs font-semibold text-gray-500 uppercase m-0">
Qty
</Text>
</Column>
<Column className="w-[30%] text-right">
<Text className="text-xs font-semibold text-gray-500 uppercase m-0">
Price
</Text>
</Column>
</Row>
{items.map((item, i) => (
<Row key={i} className="border-b border-gray-100 py-2">
<Column className="w-[50%]">
<Text className="text-sm text-gray-900 m-0">{item.name}</Text>
</Column>
<Column className="w-[20%] text-center">
<Text className="text-sm text-gray-600 m-0">{item.quantity}</Text>
</Column>
<Column className="w-[30%] text-right">
<Text className="text-sm text-gray-900 m-0">
{formatCurrency(item.price * item.quantity)}
</Text>
</Column>
</Row>
))}
</Section>
{/* Totals */}
<Section className="mt-4">
<Row>
<Column className="w-[70%] text-right">
<Text className="text-sm text-gray-600 m-0">Subtotal</Text>
</Column>
<Column className="w-[30%] text-right">
<Text className="text-sm text-gray-900 m-0">
{formatCurrency(subtotal)}
</Text>
</Column>
</Row>
<Row>
<Column className="w-[70%] text-right">
<Text className="text-sm text-gray-600 m-0">Tax</Text>
</Column>
<Column className="w-[30%] text-right">
<Text className="text-sm text-gray-900 m-0">
{formatCurrency(tax)}
</Text>
</Column>
</Row>
<Hr className="border-gray-200" />
<Row>
<Column className="w-[70%] text-right">
<Text className="text-base font-bold text-gray-900 m-0">Total</Text>
</Column>
<Column className="w-[30%] text-right">
<Text className="text-base font-bold text-gray-900 m-0">
{formatCurrency(total)}
</Text>
</Column>
</Row>
</Section>
</EmailLayout>
);
}
ReceiptEmail.PreviewProps = {
customerName: "Alex",
orderNumber: "INV-2026-001",
date: "June 4, 2026",
items: [
{ name: "Web Design Package", quantity: 1, price: 500000 },
{ name: "Domain Registration", quantity: 1, price: 1200 },
{ name: "Hosting (Annual)", quantity: 1, price: 12000 },
],
subtotal: 513200,
tax: 46188,
total: 559388,
};
Send Emails with Resend
// lib/email.ts
import { Resend } from "resend";
import WelcomeEmail from "@/emails/WelcomeEmail";
import ReceiptEmail from "@/emails/ReceiptEmail";
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendWelcomeEmail(to: string, name: string) {
await resend.emails.send({
from: "Company <noreply@example.com>",
to,
subject: `Welcome to Company, ${name}!`,
react: WelcomeEmail({ name, loginUrl: "https://example.com/login" }),
});
}
export async function sendReceipt(to: string, data: Parameters<typeof ReceiptEmail>[0]) {
await resend.emails.send({
from: "Company <billing@example.com>",
to,
subject: `Receipt for order #${data.orderNumber}`,
react: ReceiptEmail(data),
});
}
Preview Emails in Development
Add an npm script:
{
"scripts": {
"email:dev": "email dev --dir emails --port 3010"
}
}
This opens a browser preview at localhost:3010 where you can see all your templates with hot reload.
Need Transactional Email Systems?
We build email template systems, notification pipelines, and automated email workflows for SaaS products. Contact us to improve your email experience.