The react-email library by Resend lets you write email templates as React components, preview them in a local browser, and render them to battle-tested HTML with a single render() call. No more wrangling hand-coded table layouts or context-switching to a MJML file: your templates live next to your app code, accept TypeScript props, and get the same version control and component reuse as the rest of your codebase.
This tutorial covers installation, building a real template, rendering to HTML, and sending it. Code is verified against the current react-email docs (v6+).
What You Need Before Starting
- Node.js 18 or later
- A React project (or a plain Node.js backend where you’ll render and send)
- An SMTP account or a transactional email provider API key for the sending step
Step 1: Install react-email
Since react-email v6, all components and the render utility ship from a single package.
npm install react-email
If you need React as a peer dependency and it is not already installed:
npm install react react-dom
For TypeScript projects, types are included in the package.
Step 2: Start the Preview Server
The preview server lets you see your templates in a browser as you build them. Run it from the directory where your email files live:
npx react-email dev
By default the server starts on port 3000. Pass -p 3001 to use a different port. You will see every .tsx or .jsx file in your emails/ folder listed in the sidebar with live reload on save.
Step 3: Build a Template Component
Create an emails/ directory at your project root. Add a file for your first template.
emails/WelcomeEmail.tsx
import * as React from 'react';
import {
Html,
Head,
Body,
Container,
Text,
Button,
Preview,
} from 'react-email';
interface WelcomeEmailProps {
firstName: string;
confirmUrl: string;
}
export function WelcomeEmail({ firstName, confirmUrl }: WelcomeEmailProps) {
return (
<Html lang="en">
<Head />
<Preview>Welcome to the app, {firstName}!</Preview>
<Body style={bodyStyle}>
<Container style={containerStyle}>
<Text style={headingStyle}>Hey {firstName}, welcome aboard.</Text>
<Text style={textStyle}>
Your account is ready. Confirm your email address to unlock
everything.
</Text>
<Button href={confirmUrl} style={buttonStyle}>
Confirm your email
</Button>
<Text style={footerStyle}>
If you did not create this account, you can safely ignore this
email.
</Text>
</Container>
</Body>
</Html>
);
}
const bodyStyle: React.CSSProperties = {
backgroundColor: '#f6f9fc',
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const containerStyle: React.CSSProperties = {
backgroundColor: '#ffffff',
margin: '40px auto',
padding: '40px',
maxWidth: '560px',
borderRadius: '8px',
};
const headingStyle: React.CSSProperties = {
fontSize: '20px',
fontWeight: '600',
color: '#111827',
};
const textStyle: React.CSSProperties = {
fontSize: '15px',
lineHeight: '1.6',
color: '#374151',
};
const buttonStyle: React.CSSProperties = {
backgroundColor: '#2563EB',
color: '#ffffff',
padding: '12px 24px',
borderRadius: '6px',
fontSize: '15px',
fontWeight: '600',
textDecoration: 'none',
display: 'inline-block',
};
const footerStyle: React.CSSProperties = {
fontSize: '12px',
color: '#9CA3AF',
marginTop: '32px',
};
A few things to note:
<Preview>sets the hidden preheader text that email clients show in inbox previews.- Styles are inline
React.CSSPropertiesobjects. react-email handles the conversion to inline styles that survive email client CSS stripping. - Props (
firstName,confirmUrl) are plain TypeScript, so you get autocomplete and compile-time type checking everywhere the template is called.
Step 4: Render the Component to HTML
The render() function from react-email converts your component to a plain HTML string that any email provider can accept.
import { render } from 'react-email';
import { WelcomeEmail } from './emails/WelcomeEmail';
const html = await render(
<WelcomeEmail
firstName="Jordan"
confirmUrl="https://app.example.com/confirm?token=abc123"
/>
);
// html is a string of email-safe HTML ready to pass to your sending library
render() is async. It also accepts an options object: pass { plainText: true } to generate a plain-text version of the same template for the text field of your email payload.
const text = await render(
<WelcomeEmail firstName="Jordan" confirmUrl="https://..." />,
{ plainText: true }
);
Step 5: Send the Rendered HTML
Once you have the HTML string, pass it to your sending library or provider API. Below is an example using Nodemailer. For a full walk-through of Nodemailer configuration including SMTP transport, connection verification, and provider HTTP API alternatives, see how to send email in Node.js.
import nodemailer from 'nodemailer';
import { render } from 'react-email';
import { WelcomeEmail } from './emails/WelcomeEmail';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
const html = await render(
<WelcomeEmail
firstName="Jordan"
confirmUrl="https://app.example.com/confirm?token=abc123"
/>
);
await transporter.sendMail({
from: '"My App" <[email protected]>',
to: '[email protected]',
subject: 'Confirm your email address',
html,
text: await render(
<WelcomeEmail
firstName="Jordan"
confirmUrl="https://app.example.com/confirm?token=abc123"
/>,
{ plainText: true }
),
});
If you use a provider API instead of SMTP, the pattern is identical: call render(), then pass the resulting string as the html field in your API request. See the official react-email integration docs for provider-specific examples (Resend, SendGrid, Postmark, Mailgun, and others). To compare providers before committing to one, see the transactional email service comparison.
react-email vs. Hand-Coded HTML vs. MJML
| react-email | Hand-coded HTML tables | MJML | |
|---|---|---|---|
| Authoring language | TSX/JSX | Raw HTML | MJML XML |
| Type-safe props | Yes | No | No |
| Component reuse | Yes (React imports) | Copy-paste | Partial (mjml-component) |
| Live preview | Built-in dev server | Browser + email client | MJML online editor |
| Output | Email-safe HTML | Depends on skill | Email-safe HTML |
| Toolchain | Node.js / npm | Any | Node.js / npm |
| Version control fit | Excellent | Decent | Decent |
Compatibility and Limits
react-email generates HTML that targets the email client compatibility matrix maintained by the project. That said, a few caveats apply:
- Tailwind support: react-email ships a
<Tailwind>component that applies inline styles from Tailwind utility classes. This covers most clients, but CSS-in-email has known gaps (particularly in Outlook on Windows, which uses the Word rendering engine). - JavaScript in email: Rendered output is static HTML. Any interactivity (AMP for Email, interactive carousels) requires a different rendering path; react-email is not the right tool for those.
- Server-side only:
render()runs in Node.js. It is not suitable for client-side rendering in a browser without polyfilling theReadableByteStreamControllerAPI.
For a deeper look at what makes email land in the inbox rather than spam, including authentication setup, see the guide on transactional email.
Is react-email free to use?
Yes. react-email is open source (MIT license) and published by Resend at github.com/resend/react-email. There is no cost to install or use the library. Sending emails still requires a separate SMTP account or transactional email provider, each of which has its own pricing.
Do I need to use Resend to send emails with react-email?
No. react-email is a rendering library: it converts React components to HTML strings. You can pass that HTML string to any provider or SMTP server (Nodemailer, SendGrid, Postmark, Mailgun, and others). Resend is one option; it is not required.
Does react-email work with Next.js?
Yes. You can use react-email in a Next.js API route or Server Action. Call render() server-side (in a Route Handler, app/api/..., or a use server action), then pass the resulting HTML to your email provider. The preview dev server runs separately from your Next.js app and is only needed during development.
How do I add dynamic data / merge tags to a template?
Pass data as props to your component, the same way you would in any React component. The render() call accepts the JSX element with those props, so await render(<WelcomeEmail firstName="Jordan" confirmUrl="https://..." />) inlines all dynamic values at render time. There is no separate merge tag syntax to learn.
Can I use Tailwind CSS with react-email?
Yes. Wrap your template in the <Tailwind> component exported from react-email. It processes Tailwind utility classes and converts them to inline styles compatible with most email clients. Full CSS support varies by client, so test rendered output in an email preview tool before deploying.
What is the difference between react-email and @react-email/components?
As of v6, react-email is the unified package that includes all components (previously split across @react-email/components and individual packages) and the render utility. Importing from react-email is the current recommended approach. The older @react-email/components and individual component packages are deprecated.
I’ve spent my career building software at scale with a soft spot for email: deliverability, lifecycle campaigns, and getting messages to actually land. I started Coldletter to fix what bugged me about transactional and marketing email tools. I’m based in Vancouver.
