Sending emails from a web application sounds straightforward until you actually try to do it. You need an HTML template that looks decent across Gmail, Outlook, Apple Mail, and a dozen other clients. You need to inline your CSS because most email clients strip out <style> tags. You need to test on actual devices because rendering quirks vary wildly. And you need to do all of this without losing your mind.
The traditional approach involves string concatenation, template literals, or some templating language like Handlebars. You build an HTML string, inject variables, and hope the result doesn't break when rendered. It works. Barely. But maintaining those templates over time, especially as your application's design evolves, is unpleasant.
React Email changes this equation. Instead of writing raw HTML strings, you compose email templates using React components. You get type safety, component reuse, and a live preview environment. The library renders your React components to static HTML at build time or on the server, producing the exact HTML email clients need.
It's not a full-stack email service. It's the template layer — the part that creates the HTML. But it makes that part dramatically better than what came before.
At first glance, using React for email templates seems like overkill. React is a UI framework for interactive applications. Emails are static documents. They don't have state, event handlers, or re-rendering. Why bring React into this?
The answer is composition. Email templates are made of reusable pieces. Headers, footers, buttons, hero sections, order summaries — the same patterns appear across dozens of email types. Without a component model, you end up copying and pasting HTML snippets between templates. Small inconsistencies creep in. A button in the welcome email looks slightly different from a button in the password reset email. Nobody notices until a designer points it out six months later.
React components solve this naturally. You build a <Button>, a <Container>, a <Heading>, and use them everywhere. Change the button styling once, and every email template that uses that button updates automatically. The same reasons React works for web applications apply to email templates.
// A reusable button component for emails
import { Button } from "@react-email/button";
function EmailButton({ href, children }) {
return (
<Button
href={href}
style={{
backgroundColor: "#3b82f6",
color: "#ffffff",
padding: "12px 24px",
borderRadius: "6px",
fontWeight: 600,
textDecoration: "none",
display: "inline-block",
}}
>
{children}
</Button>
);
}
This button renders as a styled <a> tag with inline styles — exactly what email clients expect. The React component abstraction handles the implementation details. You just use <EmailButton> wherever you need a call to action.
The React Email library provides a set of primitives optimized for email rendering. It knows that email clients need inline styles, table-based layouts, and conservative HTML. The components handle these constraints internally.
Setting up a new email project is straightforward. You can add React Email to an existing application or create a dedicated email package within a monorepo.
npm install @react-email/components react-email
The @react-email/components package includes all the primitives: Html, Head, Body, Container, Section, Text, Link, Button, Img, and more. These replace standard HTML elements with email-safe versions that render properly across clients.
A minimal welcome email looks like this:
import {
Html,
Head,
Body,
Container,
Text,
Link,
Preview,
} from "@react-email/components";
export function WelcomeEmail({ username }) {
return (
<Html>
<Head />
<Preview>Welcome to our platform!</Preview>
<Body
style={{ backgroundColor: "#f9fafb", fontFamily: "Arial, sans-serif" }}
>
<Container
style={{ maxWidth: "600px", margin: "0 auto", padding: "40px 20px" }}
>
<Text style={{ fontSize: "24px", fontWeight: 600, color: "#111827" }}>
Welcome, {username}!
</Text>
<Text
style={{ fontSize: "16px", color: "#6b7280", lineHeight: "1.5" }}
>
Thanks for signing up. We're excited to have you on board.
</Text>
<Link
href="https://example.com/getting-started"
style={{
display: "inline-block",
backgroundColor: "#3b82f6",
color: "#ffffff",
padding: "12px 24px",
borderRadius: "6px",
textDecoration: "none",
fontWeight: 600,
marginTop: "16px",
}}
>
Get started
</Link>
</Container>
</Body>
</Html>
);
}
The <Preview> component sets the preview text that appears in inboxes next to the subject line. The <Container> handles centering and max-width. The inline styles look verbose, but they're necessary because email clients ignore external stylesheets and often strip <style> blocks.
One of React Email's best features is the development server. Running email dev starts a local server that renders your templates in real time as you edit them. You see exactly what recipients will see, across simulated desktop and mobile views.
npx email dev
The preview updates on every save. You can switch between templates, test different prop values, and verify that your layout doesn't break. This alone is a massive improvement over the old workflow of deploying to a staging environment and sending test emails to yourself.
The preview environment also validates your HTML against common email client quirks. It flags issues like unsupported CSS properties, missing alt text on images, and contrast problems. These are the kinds of things that would normally require manual testing across multiple clients.
If you've spent any time with modern CSS — flexbox, grid, custom properties, container queries — styling emails will feel like stepping back in time. Email clients support roughly the CSS that was available in 2005. Tables for layout. Inline styles. Limited font support. No JavaScript.
React Email doesn't magically fix this, but it makes it manageable. The component props accept a style object that gets inlined automatically. You don't have to manually convert CSS to inline styles — the library handles that during rendering.
For complex layouts, you still need tables. React Email provides a <Section> component that abstracts some of this away, but you'll occasionally reach for raw <table>, <tr>, and <td> elements when you need multi-column layouts.
import { Section, Column, Row } from "@react-email/components";
function TwoColumnLayout() {
return (
<Section>
<Row>
<Column style={{ width: "50%", paddingRight: "10px" }}>
<Text>Left column content</Text>
</Column>
<Column style={{ width: "50%", paddingLeft: "10px" }}>
<Text>Right column content</Text>
</Column>
</Row>
</Section>
);
}
The Column and Row components render as table cells and rows, giving you the layout control that email clients require without writing raw table markup. It's still table-based layout under the hood, but the abstraction makes it less painful.
If you've been comparing CSS Modules vs Tailwind for web applications, you'll find that email styling is a different beast entirely. Tailwind's utility classes don't work in email clients. CSS Modules' scoping doesn't matter because everything is inlined. The styling approach for email is its own thing, and React Email's inline style objects are as good as it gets.
React Email works with TypeScript out of the box. Your email components accept typed props, and the component hierarchy is type-checked. This catches mistakes that would otherwise result in broken emails discovered after sending.
interface OrderConfirmationProps {
orderNumber: string;
items: Array<{
name: string;
quantity: number;
price: number;
}>;
total: number;
customerName: string;
}
export function OrderConfirmationEmail({
orderNumber,
items,
total,
customerName,
}: OrderConfirmationProps) {
return (
<Html>
<Body>
<Text>Order #{orderNumber}</Text>
<Text>Hi {customerName},</Text>
{/* Render order items */}
{items.map((item) => (
<Text key={item.name}>
{item.name} x {item.quantity} — ${item.price}
</Text>
))}
<Text style={{ fontWeight: 700 }}>Total: ${total}</Text>
</Body>
</Html>
);
}
TypeScript ensures that every prop is provided with the correct type. If you refactor the component and change a prop name, the compiler catches every usage site. This is the same type safety you get with React components in a web application, and it's just as valuable in email templates.
If you've worked through our guide on TypeScript error handling in try catch blocks, you'll appreciate that type-safe email components eliminate a category of runtime errors. A missing prop on a web component might break interactivity. A missing prop on an email component might send a broken email to thousands of users.
React Email components render to static HTML. You call a render function that takes the component and returns an HTML string. This string is what you pass to your email provider.
import { render } from "@react-email/components";
import { WelcomeEmail } from "./emails/welcome";
const html = await render(<WelcomeEmail username="Collin" />);
// Pass `html` to your email sending service
You can render emails at build time for static templates, or at request time for dynamic content. The rendering is server-side only — there's no client-side React involved. The components are just a convenient way to construct HTML strings.
For sending, the React Email ecosystem integrates naturally with Resend, the email API built by the same team. But you can use any email provider. SendGrid, Postmark, AWS SES, Mailgun — anything that accepts an HTML string works. React Email is just the template layer.
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: "hello@example.com",
to: "user@example.com",
subject: "Welcome!",
react: <WelcomeEmail username="Collin" />,
});
Resend's SDK accepts React elements directly, which means you don't even need to call render yourself. The SDK handles rendering, inlining, and sending in a single call. For other providers, you'd render to HTML first and pass the resulting string.
One of the most useful features of React Email is the ability to test templates without actually sending emails. The preview server shows you what the email looks like. You can also write unit tests that render the component and verify the output.
import { render } from "@react-email/components";
import { WelcomeEmail } from "./emails/welcome";
test("welcome email renders username", async () => {
const html = await render(<WelcomeEmail username="Collin" />);
expect(html).toContain("Welcome, Collin!");
expect(html).toContain("Get started");
});
These tests run quickly because there's no browser involved. The components render to strings in Node.js. You can verify that dynamic content appears correctly, that conditional sections render when expected, and that links point to the right URLs.
Testing email templates used to be so cumbersome that most teams simply didn't do it. You'd manually send test emails to yourself and visually check them. React Email's testability changes the calculus. You can catch regressions in CI before they reach production.
React Email isn't the only way to build email templates. MJML is a popular alternative that uses a custom XML-like syntax. It compiles to responsive HTML that works across email clients. There's a React wrapper called mjml-react that lets you use JSX to write MJML.
The main difference is the rendering engine. MJML has its own rendering logic that's been battle-tested across thousands of email campaigns. React Email is newer but benefits from being part of the React ecosystem — the development experience is familiar, the TypeScript support is first-class, and the component model matches how frontend developers already think.
For teams already using React heavily, React Email reduces context switching. The same developers building the web application can build the email templates using the same patterns. For teams with dedicated email developers who live in MJML, that ecosystem is still excellent.
Neither approach is wrong. React Email wins on developer experience for React teams. MJML wins on rendering maturity and client compatibility. The gap is narrowing as React Email's rendering improves.
Email accessibility is often overlooked but genuinely matters. Recipients use screen readers. They view emails on small screens. They may have color vision deficiencies. Many of the web accessibility principles you already know apply to email as well.
React Email components encourage accessible defaults. The <Img> component requires an alt attribute. The <Link> component renders proper anchor tags. But you still need to make thoughtful choices about color contrast, font sizes, and content structure.
<Img
src="https://example.com/logo.png"
alt="Company Logo"
width={120}
height={40}
/>
<Text style={{ fontSize: '16px', color: '#111827' }}>
This body text meets minimum contrast requirements.
</Text>
A good rule: if your email looks like a wall of tiny gray text, it's going to be hard for many people to read. Use sufficient font sizes, maintain contrast ratios, and provide text alternatives for images. The same discipline that makes websites accessible makes emails accessible.
Before adopting React Email, I treated email templates as separate artifacts. They lived in a different part of the codebase, used a different templating language, and followed different conventions from the rest of the application. Updating an email template felt like archaeology — digging through layers of Handlebars partials and raw HTML to figure out where a change needed to go.
The first project where I used React Email was a SaaS application with about a dozen transactional emails. Welcome, password reset, invoice, team invitation, notification digest — the usual suspects. I built each one as a React component, extracted the shared pieces into a design system, and wired everything together.
The moment that sold me was when the design team updated the brand colors. In the old workflow, that would have meant finding every email template and updating hex codes manually — and inevitably missing one. With React Email, I changed the color in one shared component, and every email updated automatically. It took thirty seconds.
That kind of maintainability doesn't matter when you have three email templates. It matters enormously when you have thirty.
React Email brings the component model to email template development. Instead of wrestling with raw HTML strings and Handlebars partials, you compose templates using React components with type-safe props, live preview, and automated testing.
The rendering constraints of email clients still exist. You still need inline styles. You still need table-based layouts for complex designs. You still need to test across Gmail, Outlook, and Apple Mail. React Email doesn't eliminate these challenges, but it makes them manageable within a workflow that feels familiar to React developers.
If you're building a Next.js application with React Server Components, you can even render email templates server-side as part of your application logic. The integration is seamless because both use the same rendering model — React components that produce HTML.
For transactional emails, marketing campaigns, and any email your application sends programmatically, React Email is worth a serious look. The developer experience is a genuine step forward from the string-concatenation workflows it replaces.
Building an application that needs reliable, maintainable transactional emails? Red Surge Technology helps teams implement email systems that scale with their products. Get in touch to discuss your project.