react-email
React Email 6 is a React and TypeScript toolkit for building email markup from components, rendering those components to HTML or plain text at send time, and previewing templates in a local browser. The package now contains the layout components, Tailwind integration, rendering utilities, and the email CLI that older versions split across several @react-email packages. It helps with table-oriented email structure and known client quirks, but it is not an email delivery service and does not make browsers and inbox clients behave alike.
The strongest React-native email authoring stack when templates are real application code and the team will test in actual inboxes. It is excessive for sending existing HTML, and version 6 deserves a deliberate migration because package imports, Node support, and CLI behavior changed.
Use it if
- Your team already uses React and wants transactional email templates to accept typed props like application UI
- You want a local preview with hot reload, editable preview props, and compatibility warnings for major email clients
- You need to render HTML and plain text at send time, then hand both outputs to Resend, Nodemailer, SES, or another provider
- You maintain enough templates that shared components, Tailwind classes, and automated HTML export are worth a dedicated toolchain
- Your runtime is older than Node 20: version 6.9.2 declares Node 20 or newer, and the CLI has no legacy runtime build
- You only need to send an existing HTML string: react-email includes a preview server, compiler, components, Tailwind, syntax highlighting, and other machinery that a transport library does not need
- You plan to ship the whole package to a browser: the full package measures 503.5 KB gzipped, while rendering email belongs in a server or build step and unused modules require effective tree shaking
- Your team does not use React or prefers a markup language made specifically for email: MJML or Maizzle avoids mixing JSX, React peers, and the preview application into the workflow
- You expect visual parity from a browser preview alone: the README names testing across major clients, but Outlook, Gmail, mobile apps, dark mode, and provider rewriting still require real inbox tests
Setup reality
Install react-email plus compatible React and React DOM peers, then use Node 20 or newer. Version 6 changed the package boundary: components and render utilities are exported from react-email itself, so current examples should not begin by adding @react-email/components and @react-email/render separately. The CLI expects ./emails by default, finds .js, .jsx, and .tsx files with a default export, and recursively treats them as templates; prefix shared directories with an underscore to keep them out of the sidebar. email dev starts on port 3000 and serves emails/static for previews, but those files are not hosted for recipients. Production image URLs must be absolute and publicly reachable. PreviewProps supplies fake data in the UI, while real sends must pass validated application data to the component. email export is secondary to render-at-send-time and deletes an existing output directory before rebuilding it, so never point --outDir at a directory containing hand-edited files. Plain-text export, pretty HTML, and custom extensions are separate flags. email build copies a deployable preview app into .react-email, removes any existing folder there, installs its dependencies with npm, and email start fails until that build exists. The old --packageManager build option is now hidden, deprecated, and ignored even though older documentation may still show it. No provider credentials are needed to author, preview, render, or export. Sending still needs a separate transport and its API key or SMTP configuration. The optional email resend setup command stores a Resend API key in user-level configuration, which is unrelated to rendering templates. Tailwind styles are converted and sanitized for email, but unsupported CSS does not become universally safe; keep Head inside Tailwind when using classes that produce media rules, and verify output in actual target clients.
Patterns
Create a typed email templatecreate-email-template
import {Body, Button, Container, Head, Html, Preview, Text} from 'react-email';
type WelcomeProps = {name: string; verifyUrl: string};
export default function WelcomeEmail({name, verifyUrl}: WelcomeProps) {
return (
<Html lang="en">
<Head />
<Preview>Confirm your new account</Preview>
<Body style={{backgroundColor: '#f6f6f6', fontFamily: 'Arial, sans-serif'}}>
<Container style={{backgroundColor: '#ffffff', padding: '24px'}}>
<Text>Hello {name},</Text>
<Button href={verifyUrl} style={{backgroundColor: '#111827', color: '#ffffff', padding: '12px 18px'}}>
Verify account
</Button>
</Container>
</Body>
</Html>
);
}Version 6 exports components directly from react-email. Pass only trusted, validated URLs and user data into the template.
Supply sample data to the preview UIset-preview-props
type ReceiptProps = {customer: string; total: string};
function Receipt({customer, total}: ReceiptProps) {
return <Text>{customer}, your total is {total}.</Text>;
}
Receipt.PreviewProps = {
customer: 'Ada',
total: '$42.00',
} as ReceiptProps;
export default Receipt;PreviewProps is development sample data, not a runtime default or validation layer. Real sends must pass their own props.
Run previews from a custom template directoryrun-preview-server
npx react-email dev --dir ./src/emails --port 3001 --clients outlook,gmailThe default directory is ./emails and the default port is 3000. The clients flag narrows compatibility warnings; it does not emulate those inboxes.
Keep shared files out of the template sidebarhide-shared-components
emails/
_components/
email-footer.tsx
account-created.tsx
password-reset.tsx
static/
logo.pngThe CLI recursively discovers default-exported .js, .jsx, and .tsx files. Prefix a directory with an underscore when its files are helpers, not templates.
Inline Tailwind styles for emailstyle-with-tailwind
import {Body, Head, Html, Tailwind, Text} from 'react-email';
export default function Notice() {
return (
<Html>
<Tailwind>
<Head />
<Body className="bg-white p-6">
<Text className="text-base leading-6 text-slate-900">Payment received</Text>
</Body>
</Tailwind>
</Html>
);
}Keep Head inside Tailwind when classes generate media queries or other non-inline rules; the current component throws when it has nowhere to place them.
Switch preview assets to a hosted production URLuse-production-images
import {Img} from 'react-email';
const assetBase = process.env.NODE_ENV === 'production'
? 'https://cdn.example.com'
: '';
export function Logo() {
return <Img src={`${assetBase}/static/logo.png`} width="160" height="48" alt="Example" />;
}emails/static is served only by the local preview. A relative /static URL will not load for recipients unless your sending environment rewrites or hosts it.
Render HTML at send timerender-html
import {render} from 'react-email';
import WelcomeEmail from './emails/welcome-email';
const html = await render(
<WelcomeEmail name={user.name} verifyUrl={verifyUrl} />,
);render is asynchronous in version 6. Rendering with the real props at send time is the documented primary path.
Render a plain-text alternativerender-plain-text
import {render} from 'react-email';
const text = await render(
<WelcomeEmail name={user.name} verifyUrl={verifyUrl} />,
{plainText: true},
);Generate both HTML and text for delivery. Plain-text conversion is derived from the rendered markup, so inspect links, headings, and spacing in the result.
Send rendered output through Nodemailersend-with-nodemailer
const element = <WelcomeEmail name={user.name} verifyUrl={verifyUrl} />;
const [html, text] = await Promise.all([
render(element),
render(element, {plainText: true}),
]);
await transporter.sendMail({
from: 'Example <hello@example.com>',
to: user.email,
subject: 'Confirm your account',
html,
text,
});react-email renders content but does not deliver it. Configure, authenticate, retry, and monitor the transport separately.
Export templates as readable HTML filesexport-static-html
npx react-email export --dir ./emails --outDir ./generated-emails --prettyThe exporter removes an existing output directory before writing. Keep generated-emails dedicated to generated artifacts and out of any hand-maintained tree.
Export plain text or a custom extensionexport-plain-text
npx react-email export --dir ./emails --outDir ./text-emails --plainText
npx react-email export --dir ./emails --outDir ./shopify-emails --extension blade.phpExport has no real recipient props unless they are encoded in the template. The docs recommend it mainly for non-JavaScript backends or platforms that require manual templating.
Build and start the preview applicationdeploy-preview-app
npx react-email build --dir ./emails
npx react-email startbuild recreates .react-email and installs the preview app dependencies with npm. start requires that folder, so this is heavier than exporting email HTML.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| mjml | npm | You want a purpose-built email markup language and compiler without React component semantics |
| @maizzle/framework | npm | You prefer HTML, Tailwind CSS, layouts, and a static build pipeline for email campaigns |
| email-templates | npm | A Node service needs template rendering, localization hooks, previews, and delivery in one older-style workflow |
| nodemailer | npm | You already have HTML and plain text and only need a mature SMTP transport |