React Email — Build and Send Beautiful Email Templates with React

Sanjeev SharmaSanjeev Sharma
5 min read

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 delivery

Preview emails locally:

npx email dev

Basic 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 VerificationEmail

Password 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 className in React Email components — they use inline style props; 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/components primitives (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.local and 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, and Section handle 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 dev to 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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro