DEV Community

Atlas Whoff
Atlas Whoff

Posted on

React Email: Building Transactional Emails That Actually Render

Why Email HTML Is Different

Emails use 1998-era HTML. No CSS Grid. No Flexbox. No external stylesheets. Gmail strips <style> blocks. Outlook uses Word's rendering engine.

React Email lets you write modern JSX, then compiles it to table-based HTML that actually renders across email clients.

Setup

npm install @react-email/components react react-dom
npm install -D @react-email/render
Enter fullscreen mode Exit fullscreen mode

Your First Email Template

// emails/welcome.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Hr,
  Html,
  Img,
  Link,
  Preview,
  Section,
  Text,
} from '@react-email/components';

interface WelcomeEmailProps {
  username: string;
  verificationUrl: string;
}

export function WelcomeEmail({ username, verificationUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to Whoff Agents, {username}!</Preview>
      <Body style={main}>
        <Container style={container}>
          <Img
            src="https://whoffagents.com/logo.png"
            width="150"
            height="50"
            alt="Whoff Agents"
          />

          <Heading style={h1}>Welcome, {username}!</Heading>

          <Text style={text}>
            Thanks for signing up. Verify your email to get started.
          </Text>

          <Section style={buttonContainer}>
            <Button style={button} href={verificationUrl}>
              Verify Email
            </Button>
          </Section>

          <Text style={text}>
            Or copy this link:
            <Link href={verificationUrl}>{verificationUrl}</Link>
          </Text>

          <Hr style={hr} />

          <Text style={footer}>
            Whoff Agents · 123 Main St · San Francisco, CA 94105
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

// Inline styles (required for email)
const main = { backgroundColor: '#f6f9fc', fontFamily: '-apple-system, sans-serif' };
const container = { maxWidth: '560px', margin: '0 auto', backgroundColor: '#ffffff', padding: '40px' };
const h1 = { fontSize: '24px', fontWeight: 'bold', color: '#1a1a1a' };
const text = { fontSize: '16px', lineHeight: '24px', color: '#444444' };
const button = { backgroundColor: '#0070f3', color: '#ffffff', padding: '12px 24px', borderRadius: '6px', textDecoration: 'none', display: 'inline-block' };
const buttonContainer = { textAlign: 'center' as const, margin: '24px 0' };
const hr = { borderColor: '#e6ebf1', margin: '24px 0' };
const footer = { fontSize: '12px', color: '#8898aa' };
Enter fullscreen mode Exit fullscreen mode

Sending with Resend

npm install resend
Enter fullscreen mode Exit fullscreen mode
import { Resend } from 'resend';
import { render } from '@react-email/render';
import { WelcomeEmail } from '../emails/welcome';

const resend = new Resend(process.env.RESEND_API_KEY!);

async function sendWelcomeEmail(to: string, username: string) {
  const verificationToken = generateToken();
  const verificationUrl = `${process.env.APP_URL}/verify?token=${verificationToken}`;

  // Render React component to HTML string
  const html = render(
    <WelcomeEmail username={username} verificationUrl={verificationUrl} />
  );

  await resend.emails.send({
    from: 'hello@whoffagents.com',
    to,
    subject: `Welcome to Whoff Agents, ${username}!`,
    html,
  });
}
Enter fullscreen mode Exit fullscreen mode

Email Templates You Need for SaaS

// 1. Welcome / Email Verification
// 2. Password Reset
// 3. Magic Link Login
// 4. Subscription Confirmation
// 5. Invoice / Receipt
// 6. Trial Ending Soon
// 7. Payment Failed
// 8. Team Invitation
Enter fullscreen mode Exit fullscreen mode

Password Reset Template

export function PasswordResetEmail({ resetUrl, expiresIn = '1 hour' }: PasswordResetProps) {
  return (
    <Html>
      <Head />
      <Preview>Reset your password</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Reset your password</Heading>
          <Text style={text}>
            We received a request to reset your password. Click below to choose a new one.
          </Text>
          <Section style={buttonContainer}>
            <Button style={button} href={resetUrl}>
              Reset Password
            </Button>
          </Section>
          <Text style={smallText}>
            This link expires in {expiresIn}. If you didn't request this, ignore this email.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Preview in Browser

# React Email dev server
npx email dev
# Opens http://localhost:3000 with live preview of all templates
Enter fullscreen mode Exit fullscreen mode

You see exactly how the email looks before sending—with mobile/desktop toggle.

Testing

import { render } from '@react-email/render';
import { WelcomeEmail } from '../emails/welcome';

test('renders welcome email with username', () => {
  const html = render(
    <WelcomeEmail username="Alice" verificationUrl="https://example.com/verify?token=abc" />
  );

  expect(html).toContain('Welcome, Alice!');
  expect(html).toContain('https://example.com/verify?token=abc');
});
Enter fullscreen mode Exit fullscreen mode

Transactional email is the first thing users see after signup. Don't use a plain-text fallback.


Complete email templates (welcome, reset, invoice, trial-ending) with Resend integration: Whoff Agents AI SaaS Starter Kit.

Top comments (0)