Tutorials
10 min read

Stop Writing Raw HTML Emails. Use React Email Templates Instead.

Build maintainable, responsive email templates using React Email instead of struggling with raw HTML tables. Includes password reset and order confirmation examples.

Transactional Team
Feb 1, 2026
10 min read
Share
Stop Writing Raw HTML Emails. Use React Email Templates Instead.

A common debugging nightmare in HTML email: a single <td> is missing a style attribute, and Outlook 2019 renders the entire layout in a single column. Hours lost for a missing inline style. On a <td>.

HTML email development is stuck in 2004. You write table-based layouts with inline styles because that is the only thing that works across Outlook, Gmail, Apple Mail, Yahoo, and the dozens of other clients that all render HTML differently. There is no Flexbox. There is no Grid. There is barely CSS.

React Email fixes this. You write React components, and it compiles them to battle-tested HTML that works everywhere.

What You Will Learn

  • Why HTML email is painful and how React Email solves it
  • Building a password reset email template
  • Building an order confirmation template
  • Preview and testing workflow
  • Integrating templates with a transactional email API

Email Client CSS Support Snapshot

95%Apple Mail CSS Support
72%Gmail CSS Support
30%Outlook CSS Support
100%React Email Compatibility

Setup

mkdir email-templates && cd email-templates
npm init -y
npm install react @react-email/components
npm install -D typescript @types/react react-email

Add scripts to package.json:

{
  "scripts": {
    "dev": "email dev",
    "build": "email export"
  }
}

Create the emails directory:

mkdir -p emails

Template 1: Password Reset

This is the most common transactional email. Let us build it properly.

// emails/password-reset.tsx
import {
  Html,
  Head,
  Preview,
  Body,
  Container,
  Section,
  Text,
  Button,
  Hr,
  Img,
} from '@react-email/components';
 
interface PasswordResetProps {
  userName: string;
  resetUrl: string;
  expiresIn: string;
}
 
export default function PasswordReset({
  userName = 'User',
  resetUrl = 'https://example.com/reset?token=abc123',
  expiresIn = '1 hour',
}: PasswordResetProps) {
  return (
    <Html>
      <Head />
      <Preview>Reset your password</Preview>
      <Body style={body}>
        <Container style={container}>
          <Img
            src="https://yourdomain.com/logo.png"
            width="120"
            height="36"
            alt="Company Logo"
            style={logo}
          />
 
          <Section style={content}>
            <Text style={heading}>Reset your password</Text>
 
            <Text style={paragraph}>Hi {userName},</Text>
 
            <Text style={paragraph}>
              We received a request to reset your password. Click the
              button below to choose a new one. This link expires in{' '}
              {expiresIn}.
            </Text>
 
            <Section style={buttonContainer}>
              <Button style={button} href={resetUrl}>
                Reset Password
              </Button>
            </Section>
 
            <Text style={paragraph}>
              If you did not request this, you can safely ignore this
              email. Your password will not change.
            </Text>
 
            <Hr style={divider} />
 
            <Text style={footer}>
              If the button does not work, copy and paste this URL into
              your browser:
            </Text>
            <Text style={linkText}>{resetUrl}</Text>
          </Section>
        </Container>
      </Body>
    </Html>
  );
}
 
// Styles
const body = {
  backgroundColor: '#f4f4f5',
  fontFamily:
    '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
 
const container = {
  margin: '0 auto',
  padding: '40px 20px',
  maxWidth: '560px',
};
 
const logo = {
  margin: '0 auto 24px',
  display: 'block' as const,
};
 
const content = {
  backgroundColor: '#ffffff',
  borderRadius: '8px',
  padding: '40px 32px',
  border: '1px solid #e4e4e7',
};
 
const heading = {
  fontSize: '24px',
  fontWeight: '600' as const,
  color: '#09090b',
  marginBottom: '16px',
};
 
const paragraph = {
  fontSize: '15px',
  lineHeight: '24px',
  color: '#3f3f46',
  marginBottom: '16px',
};
 
const buttonContainer = {
  textAlign: 'center' as const,
  margin: '24px 0',
};
 
const button = {
  backgroundColor: '#18181b',
  borderRadius: '6px',
  color: '#fafafa',
  fontSize: '15px',
  fontWeight: '600' as const,
  textDecoration: 'none',
  textAlign: 'center' as const,
  padding: '12px 24px',
  display: 'inline-block' as const,
};
 
const divider = {
  borderColor: '#e4e4e7',
  margin: '24px 0',
};
 
const footer = {
  fontSize: '13px',
  color: '#71717a',
  marginBottom: '4px',
};
 
const linkText = {
  fontSize: '13px',
  color: '#3b82f6',
  wordBreak: 'break-all' as const,
};

Notice: no <table>, no <tr>, no <td>. React Email handles the table-based layout compilation. You write readable components with typed props.

Template 2: Order Confirmation

A more complex template with dynamic data:

// emails/order-confirmation.tsx
import {
  Html,
  Head,
  Preview,
  Body,
  Container,
  Section,
  Text,
  Row,
  Column,
  Hr,
  Img,
} from '@react-email/components';
 
interface OrderItem {
  name: string;
  quantity: number;
  price: number;
  imageUrl?: string;
}
 
interface OrderConfirmationProps {
  customerName: string;
  orderId: string;
  items: OrderItem[];
  subtotal: number;
  shipping: number;
  tax: number;
  total: number;
  estimatedDelivery: string;
}
 
export default function OrderConfirmation({
  customerName = 'Alex',
  orderId = 'ORD-2026-1234',
  items = [
    { name: 'Wireless Headphones', quantity: 1, price: 79.99 },
    { name: 'USB-C Cable (2m)', quantity: 2, price: 12.99 },
  ],
  subtotal = 105.97,
  shipping = 0,
  tax = 8.48,
  total = 114.45,
  estimatedDelivery = 'March 12-14, 2026',
}: OrderConfirmationProps) {
  return (
    <Html>
      <Head />
      <Preview>
        Order {orderId} confirmed - arrives by {estimatedDelivery}
      </Preview>
      <Body style={body}>
        <Container style={container}>
          <Section style={content}>
            <Text style={heading}>Order Confirmed</Text>
            <Text style={paragraph}>
              Hi {customerName}, thanks for your order. Here is your
              summary.
            </Text>
 
            <Section style={orderInfo}>
              <Row>
                <Column>
                  <Text style={label}>Order number</Text>
                  <Text style={value}>{orderId}</Text>
                </Column>
                <Column>
                  <Text style={label}>Estimated delivery</Text>
                  <Text style={value}>{estimatedDelivery}</Text>
                </Column>
              </Row>
            </Section>
 
            <Hr style={divider} />
 
            {items.map((item, index) => (
              <Section key={index} style={itemRow}>
                <Row>
                  <Column style={{ width: '70%' }}>
                    <Text style={itemName}>{item.name}</Text>
                    <Text style={itemQuantity}>
                      Qty: {item.quantity}
                    </Text>
                  </Column>
                  <Column
                    style={{
                      width: '30%',
                      textAlign: 'right' as const,
                    }}
                  >
                    <Text style={itemPrice}>
                      ${(item.price * item.quantity).toFixed(2)}
                    </Text>
                  </Column>
                </Row>
              </Section>
            ))}
 
            <Hr style={divider} />
 
            <Section style={totalsSection}>
              <Row>
                <Column>
                  <Text style={totalLabel}>Subtotal</Text>
                </Column>
                <Column style={{ textAlign: 'right' as const }}>
                  <Text style={totalValue}>
                    ${subtotal.toFixed(2)}
                  </Text>
                </Column>
              </Row>
              <Row>
                <Column>
                  <Text style={totalLabel}>Shipping</Text>
                </Column>
                <Column style={{ textAlign: 'right' as const }}>
                  <Text style={totalValue}>
                    {shipping === 0
                      ? 'Free'
                      : `$${shipping.toFixed(2)}`}
                  </Text>
                </Column>
              </Row>
              <Row>
                <Column>
                  <Text style={totalLabel}>Tax</Text>
                </Column>
                <Column style={{ textAlign: 'right' as const }}>
                  <Text style={totalValue}>${tax.toFixed(2)}</Text>
                </Column>
              </Row>
              <Hr style={divider} />
              <Row>
                <Column>
                  <Text style={grandTotalLabel}>Total</Text>
                </Column>
                <Column style={{ textAlign: 'right' as const }}>
                  <Text style={grandTotalValue}>
                    ${total.toFixed(2)}
                  </Text>
                </Column>
              </Row>
            </Section>
          </Section>
        </Container>
      </Body>
    </Html>
  );
}
 
const body = {
  backgroundColor: '#f4f4f5',
  fontFamily:
    '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
 
const container = {
  margin: '0 auto',
  padding: '40px 20px',
  maxWidth: '560px',
};
 
const content = {
  backgroundColor: '#ffffff',
  borderRadius: '8px',
  padding: '40px 32px',
  border: '1px solid #e4e4e7',
};
 
const heading = {
  fontSize: '24px',
  fontWeight: '600' as const,
  color: '#09090b',
  marginBottom: '8px',
};
 
const paragraph = {
  fontSize: '15px',
  lineHeight: '24px',
  color: '#3f3f46',
  marginBottom: '24px',
};
 
const orderInfo = {
  backgroundColor: '#f4f4f5',
  borderRadius: '6px',
  padding: '16px',
  marginBottom: '8px',
};
 
const label = {
  fontSize: '12px',
  color: '#71717a',
  textTransform: 'uppercase' as const,
  letterSpacing: '0.05em',
  marginBottom: '4px',
};
 
const value = {
  fontSize: '15px',
  fontWeight: '600' as const,
  color: '#09090b',
};
 
const divider = { borderColor: '#e4e4e7', margin: '16px 0' };
 
const itemRow = { marginBottom: '8px' };
 
const itemName = {
  fontSize: '15px',
  color: '#09090b',
  marginBottom: '2px',
};
 
const itemQuantity = { fontSize: '13px', color: '#71717a' };
 
const itemPrice = {
  fontSize: '15px',
  fontWeight: '500' as const,
  color: '#09090b',
};
 
const totalsSection = { marginTop: '8px' };
 
const totalLabel = { fontSize: '14px', color: '#71717a' };
 
const totalValue = { fontSize: '14px', color: '#3f3f46' };
 
const grandTotalLabel = {
  fontSize: '16px',
  fontWeight: '600' as const,
  color: '#09090b',
};
 
const grandTotalValue = {
  fontSize: '16px',
  fontWeight: '600' as const,
  color: '#09090b',
};

Preview and Testing

Run the dev server to preview your templates with hot reload:

npm run dev
# Opens http://localhost:3000 with a live preview

The React Email dev server shows your templates with live data. You can toggle between desktop and mobile views, swap between light and dark mode, and change prop values in real time.

Test Across Email Clients

Before sending to real users, test in multiple clients:

# Export to static HTML
npm run build
 
# The exported HTML is in the 'out' directory
# Upload to Litmus or Email on Acid for cross-client testing

Key clients to test:

  • Gmail (web and mobile) -- the most common
  • Apple Mail (macOS and iOS) -- generally the most forgiving
  • Outlook (desktop and web) -- the most problematic, uses Word's rendering engine
  • Yahoo Mail -- strips some CSS properties

Integrating with a Transactional Email API

Once your template is built, render it to HTML and send it through your email provider:

// send.ts
import { render } from '@react-email/components';
import PasswordReset from './emails/password-reset';
 
async function sendPasswordResetEmail(
  to: string,
  userName: string,
  resetToken: string
) {
  // Render the React component to HTML
  const html = await render(
    PasswordReset({
      userName,
      resetUrl: `https://app.example.com/reset?token=${resetToken}`,
      expiresIn: '1 hour',
    })
  );
 
  // Send via Transactional API
  const response = await fetch(
    'https://api.transactional.so/email/send',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.TRANSACTIONAL_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        from: 'noreply@example.com',
        to,
        subject: 'Reset your password',
        htmlBody: html,
        tag: 'password-reset',
      }),
    }
  );
 
  if (!response.ok) {
    throw new Error(`Email send failed: ${response.statusText}`);
  }
}

You can also generate a plain text version for accessibility:

import { render } from '@react-email/components';
 
const html = await render(PasswordReset({ ...props }));
const text = await render(PasswordReset({ ...props }), {
  plainText: true,
});
 
// Send both HTML and plain text versions
await sendEmail({
  htmlBody: html,
  textBody: text,
});

Template Organization

For production, organize templates with shared components:

emails/
├── components/
│   ├── header.tsx        # Logo + nav
│   ├── footer.tsx        # Unsubscribe + social links
│   ├── button.tsx        # Consistent CTA button
│   └── styles.ts         # Shared style constants
├── password-reset.tsx
├── order-confirmation.tsx
├── welcome.tsx
└── invoice.tsx

Extract shared styles:

// emails/components/styles.ts
export const colors = {
  background: '#f4f4f5',
  surface: '#ffffff',
  text: '#09090b',
  textSecondary: '#3f3f46',
  textMuted: '#71717a',
  border: '#e4e4e7',
  primary: '#18181b',
  primaryForeground: '#fafafa',
  link: '#3b82f6',
};
 
export const fonts = {
  sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
  mono: '"JetBrains Mono", Menlo, monospace',
};

The Takeaway

HTML email development is tedious, error-prone, and stuck in the past. React Email gives you components, props, TypeScript, and hot reload for email templates. You write modern React and get cross-client compatible HTML output.

The examples above cover the two most common transactional emails. Adapt the patterns for your welcome emails, invoice receipts, shipping notifications, and anything else your app sends.

If you are sending through Transactional's email API, the rendered HTML plugs directly into the htmlBody field. But the templates work with any email provider that accepts HTML input.

Sources & References

  1. [1]React Email DocumentationReact Email
  2. [2]Can I Email - CSS Support in Email ClientsCan I Email
  3. [3]Email Client CSS SupportCampaign Monitor
  4. [4]Maizzle - Framework for HTML EmailMaizzle

Written by

Transactional Team

Share
Tags:
tutorial
email
react

YOUR AGENTS DESERVE
REAL INFRASTRUCTURE.

START BUILDING AGENTS THAT DO REAL WORK.

Deploy Your First Agent