Skip to main content

How to Build Email Templates with React Email + 2026

·APIScout Team
Share:

Email HTML is notoriously awful. React Email fixes that by letting you build email templates with React components — version-controlled, type-safe, and testable. Pair it with Resend for delivery and you get a modern email stack.

TL;DR

React Email + Resend is the modern email stack for TypeScript apps. React Email handles the nightmare of HTML email compatibility — you write clean React components and it compiles them to table-based, inline-styled HTML that works across every major client. Resend delivers at 98%+ deliverability with a developer-first API that takes minutes to integrate. The free tier covers 3,000 emails/month and 100 emails/day — enough to launch a product and get through early growth. Together they replace the painful combination of hand-crafted HTML templates and legacy delivery services that most teams were using before 2023.

What You'll Build

  • Reusable email component library
  • Welcome, receipt, and notification templates
  • Live preview during development
  • Responsive email layout
  • Send via Resend API

Prerequisites: React/Next.js, Resend account (free: 3,000 emails/month).

1. Setup

Before writing a line of code, it's worth understanding why email HTML is so painful — because the problem React Email solves is not obvious until you've spent a weekend debugging it. Most web developers assume that HTML and CSS work the same everywhere. In email clients, they emphatically do not. Outlook 2016 and 2019 use Microsoft Word's rendering engine (not a browser engine) to display HTML emails. Word does not support CSS flexbox, CSS grid, or CSS animations. Gmail strips <style> block contents for forwarded emails. Yahoo Mail ignores media queries on certain devices. Apple Mail on iOS is the closest thing to a browser renderer you'll get, but it still has quirks around dark mode and dynamic font sizing. The result is that email HTML has been stuck in 2005 web techniques: table-based layouts, inline styles on every element, pixel-width columns, and fallback font stacks. React Email abstracts all of this. You write React components using primitives like <Section>, <Row>, <Column>, and <Button>, and it compiles them to battle-tested table HTML with inline styles. You never touch the compatibility layer — you just write components.

npm install @react-email/components resend
npm install -D react-email

Add to package.json:

{
  "scripts": {
    "email:dev": "email dev --dir emails",
    "email:export": "email export --dir emails --outDir out"
  }
}

2. Base Layout

Email structure best practices reflect the constraints of the rendering environments rather than modern web design preferences. Container width should be capped at 560–600px — wider than that and your email will render poorly on Outlook's default window size and on mobile clients that don't properly reflow content. The container itself should be centered with margin: 0 auto and given an explicit background color, since many clients render the email body in a gray or white container that may not match your design intent. All styles need to be inline — external stylesheets are either ignored or stripped by email clients, and <style> blocks are unreliable. The <Preview> component is important for deliverability UX: it controls the preview text that appears in inbox lists below the subject line, and it's the first thing many users read when deciding whether to open an email. Make it an extension of your subject line, not a repeat of it.

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

interface LayoutProps {
  preview: string;
  children: React.ReactNode;
}

export function Layout({ preview, children }: LayoutProps) {
  return (
    <Html>
      <Head />
      <Preview>{preview}</Preview>
      <Body style={body}>
        <Container style={container}>
          {children}
          <Hr style={hr} />
          <Text style={footer}>
            Your App Inc. · 123 Main St · San Francisco, CA 94105
            {'\n'}
            <Link href="https://yourapp.com/unsubscribe" style={link}>
              Unsubscribe
            </Link>
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

const body = {
  backgroundColor: '#f6f9fc',
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const container = {
  margin: '0 auto',
  padding: '40px 20px',
  maxWidth: '560px',
  backgroundColor: '#ffffff',
  borderRadius: '8px',
};
const hr = { borderColor: '#e6ebf1', margin: '32px 0' };
const footer = { color: '#8898aa', fontSize: '12px', lineHeight: '20px', textAlign: 'center' as const };
const link = { color: '#556cd6' };

3. Email Templates

The value of a React component library for emails is the same as for any other UI: consistency, reusability, and type safety. Before React Email, most teams maintained email HTML as a folder of .html files or Handlebars templates — copy-pasted between projects, diverging over time, impossible to refactor with confidence, and untestable without actually sending the email. A component-based approach means a change to the Layout component propagates to every email automatically. The WelcomeEmail and ReceiptEmail components below accept typed props, which means your IDE will catch a missing orderId at compile time rather than at 2am when a customer reports a blank receipt. You can also unit-test the rendering logic without an email client.

Welcome Email

// emails/WelcomeEmail.tsx
import { Heading, Text, Button, Section, Img } from '@react-email/components';
import { Layout } from './components/Layout';

interface WelcomeEmailProps {
  name: string;
  dashboardUrl: string;
}

export default function WelcomeEmail({
  name = 'there',
  dashboardUrl = 'https://yourapp.com/dashboard',
}: WelcomeEmailProps) {
  return (
    <Layout preview={`Welcome to Your App, ${name}!`}>
      <Img
        src="https://yourapp.com/logo.png"
        width={120}
        height={36}
        alt="Your App"
        style={{ margin: '0 auto 24px' }}
      />
      <Heading style={heading}>Welcome, {name}! 🎉</Heading>
      <Text style={text}>
        Your account is ready. Here are 3 things to do first:
      </Text>
      <Text style={text}>
        1. <strong>Complete your profile</strong> — Add your company and role{'\n'}
        2. <strong>Connect your first API</strong> — Import your API keys{'\n'}
        3. <strong>Invite your team</strong> — Collaborate with colleagues
      </Text>
      <Section style={buttonSection}>
        <Button style={button} href={dashboardUrl}>
          Go to Dashboard →
        </Button>
      </Section>
      <Text style={subtle}>
        Need help? Reply to this email or visit our{' '}
        <a href="https://yourapp.com/help" style={{ color: '#556cd6' }}>help center</a>.
      </Text>
    </Layout>
  );
}

const heading = { fontSize: '24px', fontWeight: 'bold', color: '#1a1a1a', margin: '0 0 16px' };
const text = { fontSize: '16px', color: '#4a4a4a', lineHeight: '26px', margin: '0 0 16px' };
const subtle = { fontSize: '14px', color: '#8a8a8a', lineHeight: '22px' };
const buttonSection = { textAlign: 'center' as const, margin: '32px 0' };
const button = {
  backgroundColor: '#2563eb',
  color: '#ffffff',
  padding: '12px 32px',
  borderRadius: '6px',
  fontSize: '16px',
  fontWeight: 'bold',
  textDecoration: 'none',
};

Receipt Email

// emails/ReceiptEmail.tsx
import { Heading, Text, Section, Row, Column, Hr } from '@react-email/components';
import { Layout } from './components/Layout';

interface LineItem {
  name: string;
  quantity: number;
  price: number;
}

interface ReceiptEmailProps {
  customerName: string;
  orderId: string;
  items: LineItem[];
  total: number;
  date: string;
}

export default function ReceiptEmail({
  customerName = 'Customer',
  orderId = '12345',
  items = [{ name: 'Pro Plan (Monthly)', quantity: 1, price: 29.00 }],
  total = 29.00,
  date = '2026-03-08',
}: ReceiptEmailProps) {
  return (
    <Layout preview={`Receipt for order #${orderId}`}>
      <Heading style={heading}>Receipt</Heading>
      <Text style={text}>Hi {customerName}, thanks for your purchase!</Text>

      <Section style={orderInfo}>
        <Row>
          <Column><Text style={label}>Order</Text><Text style={value}>#{orderId}</Text></Column>
          <Column><Text style={label}>Date</Text><Text style={value}>{date}</Text></Column>
        </Row>
      </Section>

      <Hr style={hr} />

      {items.map((item, i) => (
        <Row key={i} style={lineItem}>
          <Column style={{ width: '70%' }}>
            <Text style={itemName}>{item.name}</Text>
            <Text style={itemQty}>Qty: {item.quantity}</Text>
          </Column>
          <Column style={{ width: '30%', textAlign: 'right' }}>
            <Text style={itemPrice}>${item.price.toFixed(2)}</Text>
          </Column>
        </Row>
      ))}

      <Hr style={hr} />

      <Row>
        <Column style={{ width: '70%' }}>
          <Text style={totalLabel}>Total</Text>
        </Column>
        <Column style={{ width: '30%', textAlign: 'right' }}>
          <Text style={totalValue}>${total.toFixed(2)}</Text>
        </Column>
      </Row>
    </Layout>
  );
}

const heading = { fontSize: '24px', fontWeight: 'bold', color: '#1a1a1a' };
const text = { fontSize: '16px', color: '#4a4a4a' };
const orderInfo = { backgroundColor: '#f6f9fc', padding: '16px', borderRadius: '6px', margin: '16px 0' };
const label = { fontSize: '12px', color: '#8a8a8a', margin: '0' };
const value = { fontSize: '14px', color: '#1a1a1a', fontWeight: 'bold', margin: '4px 0 0' };
const hr = { borderColor: '#e6ebf1', margin: '16px 0' };
const lineItem = { margin: '8px 0' };
const itemName = { fontSize: '14px', color: '#1a1a1a', margin: '0' };
const itemQty = { fontSize: '12px', color: '#8a8a8a', margin: '2px 0 0' };
const itemPrice = { fontSize: '14px', color: '#1a1a1a', margin: '0' };
const totalLabel = { fontSize: '16px', fontWeight: 'bold', color: '#1a1a1a' };
const totalValue = { fontSize: '16px', fontWeight: 'bold', color: '#1a1a1a' };

4. Preview

Testing emails before sending is not optional — it is the most important step in the email development workflow. An email that looks perfect in your browser preview can look completely broken in Outlook 2019, render the wrong font in Gmail on Android, or clip the subject line on a 5-inch phone screen. React Email's preview server gives you real-time rendering in a browser so you can catch obvious layout issues early, but browser rendering is not a substitute for client testing. The preview server shows you the HTML output, not how Outlook or Gmail would actually display it. For production email, you need to test across real clients before the first send. React Email's preview makes the iteration loop fast enough that you can fix issues quickly once you identify them through client testing.

npm run email:dev

Opens at http://localhost:3000 with live reload, device preview (mobile/desktop), and source view.

5. Send with Resend

Resend's API was designed specifically for developers who are tired of SendGrid's complexity. The core API surface is a single resend.emails.send() call with four required fields: from, to, subject, and either html, text, or react. The react prop is the integration point with React Email — you pass a rendered React component directly and Resend handles the HTML compilation and inlining. Compared to SendGrid, the setup is dramatically simpler: no marketing vs. transactional distinction to navigate, no template engine to learn, no per-IP warmup required for most use cases. Resend also provides better observability out of the box — the dashboard shows delivery status, opens (with tracking pixel), and bounce/complaint rates per email with no extra configuration. For teams that have historically avoided email instrumentation because it was too complex to set up, Resend makes it the default. See the Resend vs SendGrid comparison for a detailed look at where each service wins.

// 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) {
  return resend.emails.send({
    from: 'Your App <hello@yourdomain.com>',
    to,
    subject: `Welcome to Your App, ${name}!`,
    react: WelcomeEmail({ name, dashboardUrl: 'https://yourapp.com/dashboard' }),
  });
}

export async function sendReceipt(to: string, order: any) {
  return resend.emails.send({
    from: 'Your App <billing@yourdomain.com>',
    to,
    subject: `Receipt for order #${order.id}`,
    react: ReceiptEmail({
      customerName: order.customerName,
      orderId: order.id,
      items: order.items,
      total: order.total,
      date: new Date().toLocaleDateString(),
    }),
  });
}

Email Client Compatibility

FeatureGmailOutlookApple MailYahoo
CSS inline styles
Flexbox
Grid
Media queries
Web fonts

React Email components handle these compatibility issues — they use tables and inline styles under the hood.

For production sends to large lists, supplement React Email's preview server with a dedicated cross-client testing tool. Litmus and Email on Acid both offer screenshot-based previews across 90+ client/device combinations, including real Outlook desktop renders on Windows. They're not cheap (Litmus starts around $99/month), but catching a broken Outlook layout before a 50,000-person send is worth the cost many times over. For smaller lists or early-stage products, many email services including Resend let you send test emails to seed addresses, and free Gmail/Outlook accounts cover the two most critical clients.

Common Mistakes

MistakeImpactFix
Using CSS classesStyles stripped by email clientsUse inline styles (React Email handles this)
Images without alt textBroken experience when images blockedAlways include alt text
No plain text fallbackSome clients can't render HTMLProvide text prop alongside react
Too wide layoutBreaks on mobileMax-width 600px for container
Dark mode not consideredUnreadable on dark backgroundsTest with color-scheme: light dark

Deliverability: Getting Your Email to the Inbox

React Email and Resend handle template rendering and API delivery — but inbox placement depends on factors outside both tools. Domain authentication is the foundation: SPF, DKIM, and DMARC records tell receiving mail servers that your domain is legitimately authorized to send email. Resend sets up DKIM during domain configuration and provides an SPF record to add to your DNS — follow that setup process completely before sending to real users. Sending without DKIM configured is one of the fastest ways to land in spam folders.

Domain reputation takes time to build. If you're starting with a fresh domain, warm it up gradually — send to your most engaged users first, then expand volume over two to three weeks. Sending 50,000 emails on day one from a new domain will trigger spam filters regardless of content quality.

Monitor your Resend dashboard for bounce rates and spam complaints. A hard bounce rate above 2% or a spam complaint rate above 0.1% signals a list quality problem. Hard bounces (invalid addresses) should be removed immediately and permanently — continuing to send to them damages your sender reputation cumulatively. Resend's webhook system lets you automate this: subscribe to email.bounced and email.complained events, and remove those addresses from your list in the webhook handler. The combination of proper authentication, gradual volume ramping, and active list hygiene determines whether your emails land in the inbox or the spam folder — no template library changes that math.


For a deeper look at email delivery infrastructure, read our Resend vs SendGrid comparison — deliverability rates, pricing at scale, and which API fits which use case. If you want to go further with transactional email patterns, the how to send transactional emails with Resend guide covers webhooks, bounce handling, and domain authentication in detail. For a broader survey of the email API landscape, see our best email APIs for developers roundup.

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.