React Email

Integrate React Email with Strapi v5 to build type-safe, component-based email templates. This guide covers setup, reusable layouts, and code examples.

React Email logo

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.

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:

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.1

The @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-dev

Note: 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.tsx

Start 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.com

Always 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:

FieldTypePurpose
customerNameTextRecipient's display name
customerEmailEmailDelivery address for notifications
orderNumberTextUnique order identifier
itemsJSONArray of ordered products
totalAmountDecimalOrder total
statusEnumerationpending, shipped, delivered
trackingUrlTextShipping 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 email

This 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.

Frequently Asked Questions

Yes, but they serve different purposes. Strapi's sendTemplatedEmail() uses Mustache-style syntax ({{= USER.username }}) for simple variable substitution. React Email gives you full component composition, conditional rendering, loops, and TypeScript type checking. For anything beyond basic variable replacement, React Email's render() function paired with send() provides significantly more flexibility and maintainability for your email templates.

React Email renders to standard HTML strings, making it compatible with every provider Strapi supports. The Strapi Marketplace offers official plugins for Nodemailer (any SMTP), SendGrid, Amazon SES, and Resend. Since your templates output HTML before reaching the provider layer, switching services requires changing only config/plugins.js, with no template modifications needed. Resend offers the tightest integration since they maintain React Email (see the React Email Resend integration).

No, TypeScript is optional. You can write React Email templates in plain JavaScript (.jsx files) and skip the interface definitions. However, TypeScript is strongly recommended for this integration because typed props create a contract between your Strapi content data and your email templates. This catches data mismatches at compile time rather than producing broken emails in production. The initial setup effort is minimal compared to the debugging time it saves when template data structures change.

Install the react-email CLI package as a dev dependency, then run its dev server to get a local preview UI with hot reload. Add a script at your templates directory. Run npm run email:dev, open the local URL it prints, and sanity-check layout and conditional rendering before wiring templates into Strapi.

Use Strapi’s internationalization plugin to store localized subject lines and body copy as i18n-enabled fields, then query the right locale when you build your email props. Keep the React Email component focused on layout, and pass translated strings in via props (plus set <Html lang="...">). For dates, numbers, and currency, format values with Intl.DateTimeFormat and Intl.NumberFormat based on the user’s locale, and fall back to a default locale when needed.