React Email — Build and Send Beautiful Email Templates with React
Advertisement
Introduction
Why This Matters
Email HTML is notoriously difficult. Tables, inline styles, and client quirks make standard HTML/CSS patterns unreliable. React Email provides battle-tested components that render consistently across Gmail, Outlook, Apple Mail, and mobile clients — while letting you write with familiar JSX syntax.
Installation
npm install react-email @react-email/components
npm install resend # For email deliveryPreview emails locally:
npx email devBasic Email Template Structure
// emails/verification.tsx
import {
Html,
Head,
Body,
Preview,
Container,
Section,
Heading,
Text,
Button,
Hr,
Link
} from '@react-email/components'
interface VerificationEmailProps {
name: string
verificationUrl: string
expiresInHours?: number
}
export function VerificationEmail({
name,
verificationUrl,
expiresInHours = 24
}: VerificationEmailProps) {
return (
<Html lang="en" dir="ltr">
<Head />
<Preview>Verify your email address for YourApp</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={styles.logoSection}>
<Heading style={styles.logo}>YourApp</Heading>
</Section>
<Section style={styles.content}>
<Heading style={styles.h1}>Verify your email address</Heading>
<Text style={styles.text}>Hi {name},</Text>
<Text style={styles.text}>
Thanks for signing up! Click the button below to verify your email address.
This link expires in {expiresInHours} hours.
</Text>
<Button href={verificationUrl} style={styles.button}>
Verify Email Address
</Button>
<Text style={styles.smallText}>
If the button above does not work, copy and paste this URL into your browser:
</Text>
<Link href={verificationUrl} style={styles.link}>
{verificationUrl}
</Link>
</Section>
<Hr style={styles.hr} />
<Text style={styles.footer}>
If you did not create an account, you can safely ignore this email.
</Text>
</Container>
</Body>
</Html>
)
}
const styles = {
body: { backgroundColor: '#f6f9fc', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' },
container: { backgroundColor: '#ffffff', margin: '40px auto', padding: '0 0 40px', maxWidth: '560px', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)' },
logoSection: { backgroundColor: '#1d4ed8', padding: '24px', borderRadius: '8px 8px 0 0' },
logo: { color: '#ffffff', fontSize: '24px', fontWeight: '700', margin: '0', textAlign: 'center' as const },
content: { padding: '32px 40px' },
h1: { color: '#1a202c', fontSize: '22px', fontWeight: '600', lineHeight: '1.3' },
text: { color: '#4a5568', fontSize: '15px', lineHeight: '1.6', margin: '16px 0' },
button: { backgroundColor: '#1d4ed8', borderRadius: '6px', color: '#ffffff', fontSize: '15px', fontWeight: '600', padding: '12px 24px', textDecoration: 'none', display: 'inline-block', margin: '24px 0' },
smallText: { color: '#718096', fontSize: '13px', margin: '16px 0 4px' },
link: { color: '#1d4ed8', fontSize: '13px', wordBreak: 'break-all' as const },
hr: { borderColor: '#e2e8f0', margin: '24px 40px' },
footer: { color: '#a0aec0', fontSize: '12px', textAlign: 'center' as const, padding: '0 40px' }
}
export default VerificationEmailPassword Reset Email
// emails/password-reset.tsx
import { Html, Head, Body, Preview, Container, Heading, Text, Button, Hr } from '@react-email/components'
export function PasswordResetEmail({ name, resetUrl }: { name: string; resetUrl: string }) {
return (
<Html lang="en">
<Head />
<Preview>Reset your YourApp password</Preview>
<Body style={{ backgroundColor: '#f6f9fc', fontFamily: 'sans-serif' }}>
<Container style={{ maxWidth: '560px', margin: '40px auto', backgroundColor: '#fff', borderRadius: '8px', padding: '40px' }}>
<Heading style={{ fontSize: '22px', color: '#1a202c' }}>Reset your password</Heading>
<Text style={{ color: '#4a5568', fontSize: '15px' }}>Hi {name},</Text>
<Text style={{ color: '#4a5568', fontSize: '15px' }}>
We received a request to reset your password. Click the button below to choose a new one.
This link expires in 1 hour.
</Text>
<Button href={resetUrl} style={{ backgroundColor: '#dc2626', color: '#fff', padding: '12px 24px', borderRadius: '6px', fontWeight: '600' }}>
Reset Password
</Button>
<Hr style={{ borderColor: '#e2e8f0', margin: '24px 0' }} />
<Text style={{ color: '#718096', fontSize: '12px' }}>
If you did not request a password reset, ignore this email. Your password will remain unchanged.
</Text>
</Container>
</Body>
</Html>
)
}Sending with Resend in Next.js
// app/api/auth/send-verification/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { Resend } from 'resend'
import { VerificationEmail } from '@/emails/verification'
const resend = new Resend(process.env.RESEND_API_KEY!)
export async function POST(request: NextRequest) {
try {
const { email, name, token } = await request.json()
const verificationUrl = `${process.env.NEXT_PUBLIC_APP_URL}/verify?token=${token}`
const { data, error } = await resend.emails.send({
from: 'YourApp <noreply@yourapp.com>',
to: email,
subject: 'Verify your email address',
react: VerificationEmail({ name, verificationUrl })
})
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({ id: data?.id })
} catch (err) {
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}Server Action Integration
// app/actions.ts
'use server'
import { Resend } from 'resend'
import { WelcomeEmail } from '@/emails/welcome'
const resend = new Resend(process.env.RESEND_API_KEY!)
export async function sendWelcomeEmail(userId: string) {
const user = await db.users.findUnique({ where: { id: userId } })
if (!user) return
await resend.emails.send({
from: 'team@yourapp.com',
to: user.email,
subject: 'Welcome to YourApp!',
react: WelcomeEmail({ name: user.name })
})
}Common Mistakes
- Using
classNamein React Email components — they use inlinestyleprops; Tailwind classes do not work in email clients - Using modern CSS (flexbox, grid, CSS variables) without testing — many email clients have limited CSS support
- Forgetting the
<Preview>component — preview text appears in inbox listings before the email is opened - Not testing with real email clients — the React Email preview may differ from Outlook or Gmail rendering
Best Practices
- Always use inline styles or React Email's style prop — never external CSS classes
- Use
@react-email/componentsprimitives (Button, Hr, Container) instead of plain HTML elements - Test emails with Litmus or Email on Acid before launching — client rendering varies significantly
- Store the Resend API key in
.env.localand never commit it to version control
Key Takeaways
- React Email renders to email-client-compatible HTML with inline styles automatically
- Components like
Button,Container,Hr, andSectionhandle cross-client compatibility - The
<Preview>component sets the inbox preview text — always include it - Resend is the preferred delivery service for React Email — minimal setup, good deliverability
- All styles must be inline objects — Tailwind and CSS classes are not supported in email HTML
- Use
npx email devto preview templates locally with hot reloading in a browser - Test rendered HTML across Gmail, Outlook, and Apple Mail before sending to real users
- React Email + Resend is a drop-in replacement for SendGrid or Mailgun in Next.js projects
Advertisement