These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
What Is React Email?
React Email is an open-source library that provides unstyled React components specifically designed for building transactional email templates. Instead of writing raw HTML tables and inline CSS, you compose emails using familiar components like <Button>, <Container>, <Text>, and <Section>.
The library handles the hard part: converting your React components into HTML that renders consistently across email clients like Gmail, Outlook, and Apple Mail. It follows a three-stage architecture: create templates with React components, render them to HTML strings via the render() utility, and deliver through any email service provider.
React Email supports TypeScript out of the box, offers Tailwind CSS styling via @react-email/tailwind, and works with React 18.x and 19.x (per the peerDependencies in the React Email repo). The current stable version is react-email@5.2.9 with @react-email/components@1.0.8.
Sign up for the Logbook, Strapi's Monthly newsletter
Why Integrate React Email with Strapi
Combining React Email with Strapi v5 gives you a full-stack email system where content lives in your content management system (CMS) and templates are built with components you can test and reuse. Here's what that looks like in practice:
- Type-safe template props. TypeScript interfaces on your email components catch mismatched data at compile time, not in a customer's inbox. Your template props can mirror your Strapi Content-Types directly.
- Content and code separation. Subject lines, body copy, and calls to action (CTAs) managed in Strapi update via Strapi's features without code deployments. Content editors change email copy in the Admin Panel; templates handle layout.
- Provider independence. React Email renders to standard HTML strings. Swap between Resend, SendGrid, AWS SES, or Nodemailer by changing only your
config/plugins.js. Templates stay untouched. The Strapi Marketplace has provider plugins for each. - Reusable component composition. Build shared headers, footers, button styles, and branded dividers as React components. Every transactional email in your Strapi application stays visually consistent without duplicating markup.
- Localization with Strapi i18n. Strapi's internationalization plugin manages translated email content while a single React Email template component handles layout for all locales.
- Native Strapi admin integration. Community plugins like strapi-plugin-email-designer-5 enable visual template preview and test sending directly from the admin panel.
How to Integrate React Email with Strapi
Prerequisites
Before starting, make sure you have the following in place:
- Node.js 20 or higher (React Email requires
>=20.0.0per its engine specification) - Strapi v5 project (initialized and running). If you need a fresh project:
npx create-strapi@latest my-project --quickstart- React 18.x or 19.x (required as a peer dependency for React Email)
- An SMTP (Simple Mail Transfer Protocol) server or email service account (Resend, SendGrid, AWS SES, or any SMTP provider)
- Basic familiarity with React components and Strapi's plugin configuration
Step 1: Install React Email Dependencies
From your Strapi project root, install the core React Email packages:
npm install @react-email/components@1.0.8 @react-email/render@2.0.4 react@18.3.1The @react-email/components package provides all the email-specific components through a single unified import. The @react-email/render package handles converting your components to HTML strings.
If you also want the local development preview server for designing templates, add the CLI tool as a dev dependency:
npm install react-email@5.2.9 --save-devNote: If your Strapi project already has React installed (common with custom admin panel extensions), verify the version is ^18.0 or ^19.0 to meet React Email's peer dependency requirement.
Step 2: Create Your Email Templates Directory
Set up a dedicated directory for email templates inside your Strapi src folder:
src/
└── email-templates/
├── WelcomeEmail.tsx
├── PasswordResetEmail.tsx
└── components/
├── EmailHeader.tsx
└── EmailFooter.tsxStart with a reusable layout component that all your emails will share:
// src/email-templates/components/EmailHeader.tsx
import * as React from 'react';
import { Text, Hr } from '@react-email/components';
interface EmailHeaderProps {
appName: string;
}
export function EmailHeader({ appName }: EmailHeaderProps) {
return (
<>
<Text style={{ fontSize: '24px', fontWeight: 'bold' }}>{appName}</Text>
<Hr />
</>
);
}Now create your first transactional email template:
// src/email-templates/WelcomeEmail.tsx
import * as React from 'react';
import {
Html,
Head,
Body,
Container,
Text,
Button,
Hr,
Preview,
} from '@react-email/components';
import { EmailHeader } from './components/EmailHeader';
interface WelcomeEmailProps {
userName: string;
verificationUrl: string;
}
export function WelcomeEmail({ userName, verificationUrl }: WelcomeEmailProps) {
return (
<Html lang="en">
<Head />
<Preview>Welcome to our platform. Verify your email to get started</Preview>
<Body style={{ backgroundColor: '#f6f9fc', fontFamily: 'sans-serif' }}>
<Container style={{ padding: '40px 20px', maxWidth: '560px', margin: '0 auto' }}>
<EmailHeader appName="My Application" />
<Text>Welcome, {userName}!</Text>
<Text>Please verify your email address to complete your registration.</Text>
<Button
href={verificationUrl}
style={{
backgroundColor: '#4945FF',
color: '#fff',
padding: '12px 20px',
borderRadius: '4px',
textDecoration: 'none',
}}
>
Verify Email Address
</Button>
<Hr />
<Text style={{ color: '#8898aa', fontSize: '12px' }}>
If you did not create an account, ignore this email.
</Text>
</Container>
</Body>
</Html>
);
}
export default WelcomeEmail;The <Preview> component controls the preview text shown in email clients before the recipient opens the message. This is a small detail that significantly impacts open rates.
Step 3: Configure an Email Provider in Strapi
Strapi v5's email system uses a three-part configuration structure: provider, providerOptions, and settings. The provider you choose determines how emails are delivered. Your React Email templates don't need to know or care which one you pick.
Here are two common configurations. Choose the one that fits your stack.
Option A: Nodemailer (any SMTP server)
npm install @strapi/provider-email-nodemailer// config/plugins.js
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: env('SMTP_HOST', 'smtp.example.com'),
port: env('SMTP_PORT', 587),
secure: false,
requireTLS: true,
auth: {
user: env('SMTP_USERNAME'),
pass: env('SMTP_PASSWORD'),
},
},
settings: {
defaultFrom: env('SMTP_DEFAULT_FROM'),
defaultReplyTo: env('SMTP_DEFAULT_REPLY_TO'),
},
},
},
});(Provider reference: Strapi Marketplace: Nodemailer provider)
Option B: Resend (tightest React Email integration)
npm install strapi-provider-email-resend-strapi// config/plugins.js
module.exports = ({ env }) => ({
email: {
config: {
provider: 'strapi-provider-email-resend-strapi',
providerOptions: {
apiKey: env('RESEND_API_KEY'),
},
settings: {
defaultFrom: env('EMAIL_DEFAULT_FROM', 'noreply@yourdomain.com'),
defaultReplyTo: env('EMAIL_DEFAULT_REPLY_TO', 'support@yourdomain.com'),
},
},
},
});(Provider reference: Strapi Integrations: Resend; React Email integration reference: React Email: Resend integration)
Add the corresponding environment variables to your .env file:
# For Nodemailer
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=your_username
SMTP_PASSWORD=your_password
# For Resend
RESEND_API_KEY=re_your_api_key
# Shared
EMAIL_DEFAULT_FROM=noreply@yourdomain.com
EMAIL_DEFAULT_REPLY_TO=support@yourdomain.comAlways use the env() helper from Strapi's environment configuration. Never hardcode credentials. Double-check these values in your server logs if emails fail to send. Incorrect credentials are the most common cause of delivery failures.
Step 4: Build an Email Service to Render and Send
This is where React Email and Strapi's email plugin meet. Create a service that renders your templates and passes the resulting HTML to Strapi's send() method:
// src/api/email/services/email-service.ts
import * as React from 'react';
import { render, renderPlainText } from '@react-email/render';
type EmailTemplate<T> = React.FC<T>;
export default {
async sendEmail<T>(
to: string,
subject: string,
Template: EmailTemplate<T>,
props: T
) {
const element = React.createElement(Template, props);
// Render both HTML and plain text versions
const html = render(element);
const text = renderPlainText(element);
await strapi.plugins['email'].services.email.send({
to,
subject,
html,
text,
});
},
};Including both html and text in the send() call is important. The plain text fallback ensures your emails are accessible and renders correctly in clients that strip HTML.
Step 5: Trigger Emails from Controllers or Lifecycle Hooks
With the service in place, you can send templated emails from anywhere in your Strapi application. Here's an example controller that sends a welcome email after user registration:
// src/api/auth/controllers/auth.ts
import * as React from 'react';
import { render } from '@react-email/render';
import { WelcomeEmail } from '../../../email-templates/WelcomeEmail';
export default {
async register(ctx) {
const { email, username } = ctx.request.body;
// Your registration logic here...
const user = await strapi.documents('api::user.user').create({
data: { email, username },
});
const verificationUrl = `${process.env.APP_URL}/verify?token=${user.verificationToken}`;
// Render the React Email template to HTML
const html = render(
React.createElement(WelcomeEmail, {
userName: username,
verificationUrl,
})
);
// Send welcome email using the email service
await strapi
.plugin('email')
.service('email')
.send({
to: user.email,
subject: 'Welcome: Please Verify Your Email',
html,
});
ctx.body = { message: 'Registration successful. Check your email.' };
},
};You can also trigger emails from lifecycle hooks on your Content-Types. For instance, sending a notification when a new order is created:
// src/api/order/content-types/order/lifecycles.js
const { render } = require('@react-email/render');
const React = require('react');
const { NotificationEmail } = require('../../../../email-templates/NotificationEmail');
module.exports = {
async afterCreate(event) {
const { result } = event;
const html = render(
React.createElement(NotificationEmail, {
userName: result.customerName,
notificationType: 'Order Confirmation',
message: `Your order #${result.id} has been placed successfully.`,
actionUrl: `${process.env.APP_URL}/orders/${result.id}`,
})
);
await strapi.plugin('email').service('email').send({
to: result.customerEmail,
subject: `Order #${result.id} Confirmed`,
html,
});
},
};The file must be named exactly lifecycles.js and placed at src/api/[content-type]/content-types/[content-type]/lifecycles.js for Strapi to detect it.
Step 6: Create a Custom Email Provider (Optional)
If you need more control over the email transport, say for logging every outbound email, adding custom headers, or integrating a provider that doesn't have a Strapi plugin, you can build a custom provider. Strapi v5 uses a factory pattern: export an object with an init() function that returns an object containing send().
// src/email-provider/index.js
const nodemailer = require('nodemailer');
module.exports = {
init: (providerOptions = {}, settings = {}) => {
const transporter = nodemailer.createTransport({
host: providerOptions.host,
port: providerOptions.port || 587,
auth: {
user: providerOptions.user,
pass: providerOptions.pass,
},
});
return {
send: async (options) => {
const {
from = settings.defaultFrom,
to,
replyTo = settings.defaultReplyTo,
subject,
text,
html,
...rest
} = options;
if (!to) throw new Error('Recipient (to) is required');
// Log outbound emails in development
if (process.env.NODE_ENV === 'development') {
strapi.log.info(`Sending email to ${to}: ${subject}`);
}
await transporter.sendMail({
from,
to,
replyTo,
subject,
text,
html,
...rest,
});
},
};
},
};Reference this local provider in your config:
// config/plugins.js
module.exports = ({ env }) => ({
email: {
config: {
provider: require('../src/email-provider'),
providerOptions: {
host: env('SMTP_HOST'),
port: env('SMTP_PORT', 587),
user: env('SMTP_USERNAME'),
pass: env('SMTP_PASSWORD'),
},
settings: {
defaultFrom: 'noreply@myapp.com',
defaultReplyTo: 'support@myapp.com',
},
},
},
});Your custom provider completely replaces Strapi's default Sendmail provider. The send() method receives already-rendered HTML when called through strapi.plugin('email').service('email').send(), so React Email's render() output flows through cleanly.
Project Example: E-Commerce Order Notification System
Let's build a practical order notification system where Strapi manages product and order data, and React Email handles all transactional emails: order confirmation, shipping updates, and delivery notifications. This example demonstrates how parts of the Strapi integration ecosystem can work together.
Define the Strapi Content-Types
First, create an Order Collection Type in Strapi with the following fields:
| Field | Type | Purpose |
|---|---|---|
customerName | Text | Recipient's display name |
customerEmail | Delivery address for notifications | |
orderNumber | Text | Unique order identifier |
items | JSON | Array of ordered products |
totalAmount | Decimal | Order total |
status | Enumeration | pending, shipped, delivered |
trackingUrl | Text | Shipping tracker link |
Build the Email Templates
Create a set of order-related email templates that share a common layout:
// src/email-templates/components/OrderLayout.tsx
import * as React from 'react';
import {
Html,
Head,
Body,
Container,
Text,
Hr,
Preview,
} from '@react-email/components';
interface OrderLayoutProps {
previewText: string;
children: React.ReactNode;
}
export function OrderLayout({ previewText, children }: OrderLayoutProps) {
return (
<Html lang="en">
<Head />
<Preview>{previewText}</Preview>
<Body style={{ backgroundColor: '#f6f9fc', fontFamily: 'sans-serif' }}>
<Container style={{ padding: '40px 20px', maxWidth: '560px', margin: '0 auto', backgroundColor: '#ffffff' }}>
<Text style={{ fontSize: '20px', fontWeight: 'bold', color: '#4945FF' }}>
My Store
</Text>
<Hr />
{children}
<Hr />
<Text style={{ color: '#8898aa', fontSize: '12px' }}>
Questions? Reply to this email or contact support@mystore.com
</Text>
</Container>
</Body>
</Html>
);
}// src/email-templates/OrderConfirmationEmail.tsx
import * as React from 'react';
import { Text, Button, Section } from '@react-email/components';
import { OrderLayout } from './components/OrderLayout';
interface OrderItem {
name: string;
quantity: number;
price: number;
}
interface OrderConfirmationEmailProps {
customerName: string;
orderNumber: string;
items: OrderItem[];
totalAmount: number;
orderUrl: string;
}
export function OrderConfirmationEmail({
customerName,
orderNumber,
items,
totalAmount,
orderUrl,
}: OrderConfirmationEmailProps) {
return (
<OrderLayout previewText={`Order ${orderNumber} confirmed`}>
<Text>Hi {customerName},</Text>
<Text>Your order <strong>#{orderNumber}</strong> has been confirmed.</Text>
<Section style={{ padding: '16px', backgroundColor: '#f6f9fc', borderRadius: '4px' }}>
{items.map((item, index) => (
<Text key={index} style={{ margin: '4px 0' }}>
{item.name} × {item.quantity} — ${item.price.toFixed(2)}
</Text>
))}
<Text style={{ fontWeight: 'bold', marginTop: '12px' }}>
Total: ${totalAmount.toFixed(2)}
</Text>
</Section>
<Button
href={orderUrl}
style={{
backgroundColor: '#4945FF',
color: '#fff',
padding: '12px 20px',
borderRadius: '4px',
textDecoration: 'none',
marginTop: '16px',
}}
>
View Order
</Button>
</OrderLayout>
);
}
export default OrderConfirmationEmail;// src/email-templates/ShippingUpdateEmail.tsx
import * as React from 'react';
import { Text, Button } from '@react-email/components';
import { OrderLayout } from './components/OrderLayout';
interface ShippingUpdateEmailProps {
customerName: string;
orderNumber: string;
status: 'shipped' | 'delivered';
trackingUrl?: string;
}
export function ShippingUpdateEmail({
customerName,
orderNumber,
status,
trackingUrl,
}: ShippingUpdateEmailProps) {
const statusMessages = {
shipped: `Your order #${orderNumber} has been shipped and is on its way!`,
delivered: `Your order #${orderNumber} has been delivered.`,
};
return (
<OrderLayout previewText={`Order ${orderNumber}: ${status}`}>
<Text>Hi {customerName},</Text>
<Text>{statusMessages[status]}</Text>
{trackingUrl && status === 'shipped' && (
<Button
href={trackingUrl}
style={{
backgroundColor: '#4945FF',
color: '#fff',
padding: '12px 20px',
borderRadius: '4px',
textDecoration: 'none',
}}
>
Track Shipment
</Button>
)}
</OrderLayout>
);
}
export default ShippingUpdateEmail;Wire Up Lifecycle Hooks
Use Strapi lifecycle hooks to automatically send the right email when an order's status changes:
// src/api/order/content-types/order/lifecycles.js
const React = require('react');
const { render, renderPlainText } = require('@react-email/render');
const { OrderConfirmationEmail } = require('../../../../email-templates/OrderConfirmationEmail');
const { ShippingUpdateEmail } = require('../../../../email-templates/ShippingUpdateEmail');
module.exports = {
async afterCreate(event) {
const { result } = event;
const element = React.createElement(OrderConfirmationEmail, {
customerName: result.customerName,
orderNumber: result.orderNumber,
items: result.items,
totalAmount: result.totalAmount,
orderUrl: `${process.env.APP_URL}/orders/${result.orderNumber}`,
});
const html = render(element);
const text = renderPlainText(element);
await strapi.plugins['email'].services.email.send({
to: result.customerEmail,
subject: `Order #${result.orderNumber} Confirmed`,
html,
text,
});
},
async afterUpdate(event) {
const { result, params } = event;
// Only send if status actually changed
const statusChanged = params.data?.status && params.data.status !== result.status;
if (!statusChanged) return;
const newStatus = params.data.status;
if (newStatus !== 'shipped' && newStatus !== 'delivered') return;
const element = React.createElement(ShippingUpdateEmail, {
customerName: result.customerName,
orderNumber: result.orderNumber,
status: newStatus,
trackingUrl: result.trackingUrl,
});
const html = render(element);
const text = renderPlainText(element);
await strapi.plugin('email').service('email').send({
to: result.customerEmail,
subject: `Order #${result.orderNumber}: ${newStatus === 'shipped' ? 'Shipped' : 'Delivered'}`,
html,
text,
});
},
};The complete data flow works like this:
Order created/updated in Strapi Admin Panel or via REST API
→ Lifecycle hook fires (afterCreate / afterUpdate)
→ React Email template receives order data as typed props
→ render() converts component to HTML string
→ strapi.plugin('email').service('email').send() delivers via configured provider
→ Customer receives a well-formatted, consistent emailThis pattern gives you type-safe templates that are easy to test in isolation, content that flows from Strapi's data layer, and delivery that works with any provider you configure. When you need to add a new email type, such as refund confirmation, review request, or back-in-stock notification, you create a new React component and hook it up. No HTML string concatenation, no template engine syntax to learn.
Strapi Open Office Hours
If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.
For more details, visit the Strapi documentation and React Email documentation.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.