
React Email
- 6.2k installs
- 159 repo stars
- Updated July 23, 2026
- resend/resend-skills
This is a copy of react-email by resend - installs and ranking accrue to the original listing.
react-email is a Claude skill that generates production-ready, email-client-compatible HTML from React components using React Email and Tailwind-like styling for developers building transactional or marketing emails.
About
react-email is a Claude skill from Resend that serves as a complete reference for building emails with the React Email component library. It documents components such as Body, Button, CodeBlock, CodeInline, and Column, all imported from the react-email package and styled via the Tailwind helper for cross-client compatibility. The skill enforces correct import usage and email-safe HTML patterns so output renders in major clients rather than breaking like typical web React. Developers reach for react-email when authoring password resets, receipts, newsletters, or onboarding messages as React components destined for Resend or similar send APIs.
- Complete reference for all React Email components including Body, Button, Container, Heading, Preview, Row, Column and m
- All components imported from a single react-email package to minimize bundle size
- Tailwind styling support with zero-config email rendering
- Specialized components such as CodeBlock with Prism.js highlighting, Markdown renderer, and Font loader
- Strict rule to only import components actually used in each template
React Email by the numbers
- 6,232 all-time installs (skills.sh)
- +513 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/resend/resend-skills --skill react-emailAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6.2k |
|---|---|
| repo stars | ★ 159 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 23, 2026 |
| Repository | resend/resend-skills ↗ |
How do you build email-safe HTML with React components?
Generate production-ready, email-client-compatible HTML from React components using a familiar Tailwind-like syntax.
Who is it for?
Frontend and full-stack developers sending transactional or marketing email through Resend who prefer React components over hand-written table HTML.
Skip if: Developers building in-app UI with standard React DOM or teams that only need plain-text emails without HTML templates.
When should I use this skill?
User asks to build or style a transactional email, newsletter template, or React Email component for Resend delivery.
What you get
React Email component source files and rendered HTML compatible with major email clients, ready for Resend or SMTP delivery.
- React Email component source
- Email-client-compatible HTML output
- Styled transactional or marketing template
By the numbers
- References 5+ React Email components including Body, Button, CodeBlock, CodeInline, and Column
Files
React Email
Build and send HTML emails using React components - a modern, component-based approach to email development that works across all major email clients.
Installation
npm i react-emailOr scaffold a new project:
npx create-email@latest
cd react-email-starter
npm install
npm run devThis works with any package manager (npm, yarn, pnpm, bun) — substitute accordingly.
The dev server runs at localhost:3000 with a preview interface for templates in the emails folder.
Adding to an Existing Project
Install the packages and add a script to your package.json:
{
"scripts": {
"email": "email dev --dir emails --port 3000"
}
}Make sure the path to the emails folder is relative to the base project directory. Ensure tsconfig.json includes proper support for JSX.
Basic Email Template
Create an email component with proper structure using the Tailwind component for styling:
import {
Html,
Head,
Preview,
Body,
Container,
Heading,
Text,
Button,
Tailwind,
pixelBasedPreset
} from 'react-email';
interface WelcomeEmailProps {
name: string;
verificationUrl: string;
}
export default function WelcomeEmail({ name, verificationUrl }: WelcomeEmailProps) {
return (
<Html lang="en">
<Tailwind
config={{
presets: [pixelBasedPreset],
theme: {
extend: {
colors: {
brand: '#007bff',
},
},
},
}}
>
<Head />
<Body className="bg-gray-100 font-sans">
<Preview>Welcome - Verify your email</Preview>
<Container className="max-w-xl mx-auto p-5">
<Heading className="text-2xl text-gray-800">
Welcome!
</Heading>
<Text className="text-base text-gray-800">
Hi {name}, thanks for signing up!
</Text>
<Button
href={verificationUrl}
className="bg-brand text-white px-5 py-3 rounded block text-center no-underline box-border"
>
Verify Email
</Button>
</Container>
</Body>
</Tailwind>
</Html>
);
}
// Preview props for testing
WelcomeEmail.PreviewProps = {
name: 'John Doe',
verificationUrl: 'https://example.com/verify/abc123'
} satisfies WelcomeEmailProps;
export { WelcomeEmail };Behavioral Guidelines
- When iterating over the code, only update what the user asked for. Keep the rest intact.
- If the user asks to use media queries, inform them that most email clients don't support them and suggest a different approach.
- Never use template variables (like
{{name}}) directly in TypeScript code. Instead, reference the underlying properties directly. If the user explicitly asks for{{variableName}}, place the mustache string only in PreviewProps, never in the component JSX:
const EmailTemplate = (props) => {
return (
<h1>Hello, {props.variableName}!</h1>
);
}
EmailTemplate.PreviewProps = {
variableName: "{{variableName}}",
};
export default EmailTemplate;- Never write the
{{variableName}}pattern directly in the component structure. If the user insists, explain that this would make the template invalid.
Essential Components
See references/COMPONENTS.md for complete component documentation.
Core Structure:
Html- Root wrapper withlangattributeHead- Meta elements, styles, fontsBody- Main content wrapperContainer- Outermost centering wrapper (has built-inmax-width: 37.5em). Use only once per email.Section- Interior content blocks (no built-in max-width). Use for grouping content insideContainer.Row&Column- Multi-column layoutsTailwind- Enables Tailwind CSS utility classes
Content:
Preview- Inbox preview text, always first inside<Body>Heading- h1-h6 headingsText- ParagraphsButton- Styled link buttons (always includebox-border)Link- HyperlinksImg- Images (see Static Files section below)Hr- Horizontal dividers
Specialized:
CodeBlock- Syntax-highlighted codeCodeInline- Inline codeMarkdown- Render markdownFont- Custom web fonts
Before Writing Code
When a user requests an email template, ask clarifying questions FIRST if they haven't provided:
1. Brand colors - Ask for primary brand color (hex code like #007bff) 2. Logo - Ask if they have a logo file and its format (PNG/JPG only - warn if SVG/WEBP) 3. Style preference - Professional, casual, or minimal tone 4. Production URL - Where will static assets be hosted in production?
Static Files and Images
Directory Structure
Local images must be placed in the static folder inside your emails directory:
project/
├── emails/
│ ├── welcome.tsx
│ └── static/ <-- Images go here
│ └── logo.pngDev vs Production URLs
Use this pattern for images that work in both dev preview and production:
const baseURL = process.env.NODE_ENV === "production"
? "https://cdn.example.com" // User's production CDN
: "";
export default function Email() {
return (
<Img
src={`${baseURL}/static/logo.png`}
alt="Logo"
width="150"
height="50"
/>
);
}How it works:
- Development:
baseURLis empty, so URL is/static/logo.png- served by React Email's dev server - Production:
baseURLis the CDN domain, so URL ishttps://cdn.example.com/static/logo.png
Important: Always ask the user for their production hosting URL. Do not hardcode localhost:3000.
Styling
See references/STYLING.md for comprehensive styling documentation including typography, layout patterns, dark mode, and brand consistency.
Key Rules
- Use
TailwindwithpixelBasedPreset(email clients don't supportrem). ImportpixelBasedPresetfromreact-email. - Never use flexbox or grid — use
Row/Columncomponents or tables for layouts. - Avoid CSS/Tailwind media queries (
sm:,md:,lg:,xl:) — limited email client support. - Never use theme selectors (
dark:,light:) — not supported. - Never use SVG or WEBP images — warn users about rendering issues.
- Always specify border type (
border-solid,border-dashed, etc.) — email clients don't inherit it. - For single-side borders, reset others first (
border-none border-l border-solid).
Required Classes
| Component | Required Class | Why |
|---|---|---|
Button | box-border | Prevents padding from overflowing the button width |
Hr / any border | border-solid (or border-dashed, etc.) | Email clients don't inherit border type |
| Single-side borders | border-none + the side | Resets default borders on other sides |
Structure Notes
- Always define
<Head />inside<Tailwind>when using Tailwind CSS <Preview>should always be the first element inside<Body>- Only include props in
PreviewPropsthat the component actually uses - Use fixed width/height for known-size elements (logos, icons); responsive sizing (
w-full,h-auto) for content images
Rendering
Convert to HTML
import { render } from 'react-email';
import { WelcomeEmail } from './emails/welcome';
const html = await render(
<WelcomeEmail name="John" verificationUrl="https://example.com/verify" />
);Convert to Plain Text
const text = await render(<WelcomeEmail name="John" verificationUrl="https://example.com/verify" />, { plainText: true });Sending
React Email supports sending with any email service provider. See references/SENDING.md for complete sending documentation including Resend, Nodemailer, and SendGrid examples.
Quick example using the Resend SDK:
import { Resend } from 'resend';
import { WelcomeEmail } from './emails/welcome';
const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: 'Acme <onboarding@resend.dev>',
to: ['user@example.com'],
subject: 'Welcome to Acme',
react: <WelcomeEmail name="John" verificationUrl="https://example.com/verify" />
});The Resend Node SDK automatically handles both HTML and plain-text rendering.
CLI Commands
The react-email package provides a CLI accessible via the email command:
| Command | Description |
|---|---|
email dev --dir <path> --port <port> | Start the preview development server (default: ./emails, port 3000) |
email build --dir <path> | Build the preview app for production deployment |
email start | Run the built preview app |
email export --outDir <path> --pretty --plainText --dir <path> | Export templates to static HTML files |
email resend setup | Connect the CLI to your Resend account via API key |
email resend reset | Remove the stored Resend API key |
Internationalization
See references/I18N.md for complete i18n documentation. React Email supports three libraries: next-intl, react-i18next, and react-intl.
Email Editor
React Email includes a visual editor (@react-email/editor) that can be embedded in your app. It's built on TipTap/ProseMirror and produces email-ready HTML.
See references/EDITOR.md for complete documentation including:
EmailEditor— batteries-included component with bubble menus, slash commands, and themingStarterKit— 35+ email-aware extensions (headings, lists, tables, columns, buttons, etc.)Inspector— contextual sidebar for editing stylesEmailTheming— built-in themes (basic,minimal) with customizable CSS propertiescomposeReactEmail— export editor content to email-ready HTML and plain text- Custom extensions via
EmailNodeandEmailMark
Quick example:
import { EmailEditor, type EmailEditorRef } from '@react-email/editor';
import '@react-email/editor/themes/default.css';
import { useRef } from 'react';
export function MyEditor() {
const ref = useRef<EmailEditorRef>(null);
return (
<EmailEditor
ref={ref}
content="<p>Start typing...</p>"
theme="basic"
/>
);
}Common Patterns
See references/PATTERNS.md for complete examples including:
- Password reset emails
- Order confirmations with product lists
- Notification emails with code blocks
- Multi-column layouts
- Team invitation emails
Email Best Practices
1. Test across email clients - Gmail, Outlook, Apple Mail, Yahoo Mail 2. Keep it responsive - Max-width around 600px, test on mobile 3. Use absolute image URLs - Host on reliable CDN, always include alt text 4. Provide plain text version - Required for accessibility 5. Keep file size under 102KB - Gmail clips larger emails 6. Add proper TypeScript types - Define interfaces for all email props 7. Include preview props - Add .PreviewProps for development testing 8. Use verified domains - For production from addresses
Additional Resources
- React Email Documentation
- React Email GitHub
- Resend Documentation
- Email Client CSS Support
- Component Reference: references/COMPONENTS.md
- Styling Guide: references/STYLING.md
- Email Editor: references/EDITOR.md
- Sending Guide: references/SENDING.md
- Internationalization Guide: references/I18N.md
- Common Patterns: references/PATTERNS.md
React Email Components Reference
Complete reference for all React Email components. All examples use the Tailwind component for styling.
Important: Only import the components you need. Do not use components in the code if you are not importing them.
Available Components
All components are imported from react-email:
- Body - A React component to wrap emails
- Button - A link that is styled to look like a button
- CodeBlock - Display code with a selected theme and regex highlighting using Prism.js
- CodeInline - Display a predictable inline code HTML element that works on all email clients
- Column - Display a column that separates content areas vertically in your email (must be used with Row)
- Container - A layout component that centers your content horizontally on a breaking point
- Font - A React Font component to set your fonts
- Head - Contains head components, related to the document such as style and meta elements
- Heading - A block of heading text
- Hr - Display a divider that separates content areas in your email
- Html - A React html component to wrap emails
- Img - Display an image in your email
- Link - A hyperlink to web pages, email addresses, or anything else a URL can address
- Markdown - A Markdown component that converts markdown to valid react-email template code
- Preview - A preview text that will be displayed in the inbox of the recipient
- Row - Display a row that separates content areas horizontally in your email
- Section - Display a section that can also be formatted using rows and columns
- Tailwind - A React component to wrap emails with Tailwind CSS
- Text - A block of text separated by blank spaces
Tailwind
The recommended way to style React Email components. Wrap your email content and use utility classes.
import { Tailwind, pixelBasedPreset, Html, Body, Container, Heading, Text, Button } from 'react-email';
export default function Email() {
return (
<Html lang="en">
<Tailwind
config={{
presets: [pixelBasedPreset],
theme: {
extend: {
colors: {
brand: '#007bff',
accent: '#28a745'
},
},
},
}}
>
<Body className="bg-gray-100 font-sans">
<Container className="max-w-xl mx-auto p-5">
<Heading className="text-2xl font-bold text-brand mb-4">
Welcome!
</Heading>
<Text className="text-base text-gray-700 mb-4">
Your content here.
</Text>
<Button
href="https://example.com"
className="bg-brand text-white px-6 py-3 rounded-lg block text-center box-border"
>
Get Started
</Button>
</Container>
</Body>
</Tailwind>
</Html>
);
}Props:
config- Tailwind configuration object
How it works:
- Tailwind classes are converted to inline styles automatically
- Media queries are extracted to
<style>tag in<head> - CSS variables are resolved
- RGB color syntax is normalized for email client compatibility
Important:
- Always use
pixelBasedPreset- email clients don't supportremunits - Custom config is optional - defaults work well
- Avoid responsive classes (sm:, md:, lg:). These have limited email client support, and are not reliable across major clients
Structural Components
Html
Root wrapper for the email. Always use as the outermost component.
import { Html, Tailwind, pixelBasedPreset } from 'react-email';
<Html lang="en" dir="ltr">
<Tailwind config={{ presets: [pixelBasedPreset] }}>
{/* email content */}
</Tailwind>
</Html>Props:
lang- Language code (e.g., "en", "es", "fr")dir- Text direction ("ltr" or "rtl")
Head
Contains head components, related to the document such as style and meta elements. Place inside <Tailwind>.
import { Head } from 'react-email';
<Head>
<title>Email Title</title>
</Head>Body
A React component to wrap emails.
import { Body } from 'react-email';
<Body className="bg-gray-100 font-sans">
{/* email content */}
</Body>Container
A layout component that centers your content horizontally on a breaking point. Has a max-width constraint of 37.5em.
import { Container } from 'react-email';
<Container className="max-w-xl mx-auto p-5">
{/* centered content */}
</Container>Section
Display a section that can also be formatted using rows and columns.
import { Section } from 'react-email';
<Section className="p-5 bg-white">
{/* section content */}
</Section>Row & Column
Row displays content areas horizontally, Column displays content areas vertically. A Column needs to be used in combination with a Row component.
import { Section, Row, Column } from 'react-email';
<Section>
<Row>
<Column className="w-1/2 p-2 align-top">
Left column content
</Column>
<Column className="w-1/2 p-2 align-top">
Right column content
</Column>
</Row>
</Section>Column widths:
- Use percentage widths (e.g., "w-1/2", "w-1/3")
- Or use Tailwind's width utilities
- Total should add up to 100% or container width
Content Components
Preview
A preview text that will be displayed in the inbox of the recipient.
import { Preview } from 'react-email';
<Preview>Welcome to our platform - Get started today!</Preview>Best practices:
- Keep under 140 characters
- Make it compelling and action-oriented
- Should always be the first element inside
<Body>
Heading
A block of heading text (h1-h6).
import { Heading } from 'react-email';
<Heading as="h1" className="text-2xl font-bold text-gray-800 mb-4">
Welcome to Acme
</Heading>
<Heading as="h2" className="text-xl font-semibold text-gray-600 mb-3">
Getting Started
</Heading>Props:
as- HTML heading level ("h1" through "h6")
Text
A block of text separated by blank spaces.
import { Text } from 'react-email';
<Text className="text-base leading-6 text-gray-800 my-4">
Your paragraph content here.
</Text>Button
A link that is styled to look like a button. Has workaround for padding issues in Outlook.
import { Button } from 'react-email';
<Button
href="https://example.com/verify"
target="_blank"
className="bg-blue-600 text-white px-5 py-3 rounded block text-center no-underline font-medium box-border"
>
Verify Email Address
</Button>Props:
href(required) - URL to link totarget- Default is "_blank"
Styling tips:
- Use
blockfor full-width buttons - Use
text-centerfor centered text - Add
no-underlineto remove underline
Link
A hyperlink to web pages, email addresses, or anything else a URL can address.
import { Link } from 'react-email';
<Link href="https://example.com" target="_blank" className="text-blue-600 underline">
Visit our website
</Link>Props:
href(required) - URL to link totarget- Default is "_blank"
Img
Display an image in your email.
import { Img } from 'react-email';
<Img
src="https://example.com/logo.png"
alt="Company Logo"
width="150"
height="50"
className="block mx-auto"
/>Props:
src(required) - Image URL (must be absolute)alt(required) - Alt text for accessibilitywidth- Image width in pixelsheight- Image height in pixels
Best practices:
- Always use absolute URLs hosted on CDN
- Always include alt text
- Specify width and height to prevent layout shift
- Use
blockclass to avoid spacing issues
Hr
Display a divider that separates content areas in your email.
import { Hr } from 'react-email';
<Hr className="border-solid border-gray-200 my-5" />Specialized Components
CodeBlock
Display code with a selected theme and regex highlighting using Prism.js.
import { CodeBlock, dracula } from 'react-email';
const Email = () => {
const code = `export default async (req, res) => {
try {
const html = await render(
<EmailTemplate firstName="John" />
);
return NextResponse.json({ html });
} catch (error) {
return NextResponse.json({ error });
}
}`;
return (
<div className="overflow-auto">
<CodeBlock
fontFamily="monospace"
theme={dracula}
language="javascript"
code={code}
/>
</div>
);
};Props:
code(required) - The actual code to render in the code block. Just a plain string, with the proper indentation includedlanguage(required) - The language under the supported languages defined in PrismLanguage (e.g., "javascript", "python", "typescript")theme(required) - The theme to use for the code block (import from "react-email": dracula, github, nord, etc.)fontFamily(optional) - The font family to use for the code block (e.g., "monospace")lineNumbers(optional) - Whether or not to automatically include line numbers on the rendered code block (boolean, default: false)
Important:
- By default, do not use the
lineNumbersprop unless specifically requested - Always wrap the
CodeBlockcomponent in adivtag with theoverflow-autoclass to avoid padding overflow
CodeInline
Display a predictable inline code HTML element that works on all email clients.
import { Text, CodeInline } from 'react-email';
<Text className="text-base text-gray-800">
Run <CodeInline className="bg-gray-100 px-1 rounded">npm install</CodeInline> to get started.
</Text>Markdown
A Markdown component that converts markdown to valid react-email template code.
import { Html, Markdown } from 'react-email';
const Email = () => {
return (
<Html lang="en" dir="ltr">
<Markdown
markdownCustomStyles={{
h1: { color: "red" },
h2: { color: "blue" },
codeInline: { background: "grey" },
}}
markdownContainerStyles={{
padding: "12px",
border: "solid 1px black",
}}
>{`# Hello, World!`}</Markdown>
{/* OR */}
<Markdown children={`# This is a ~~strikethrough~~`} />
</Html>
);
};Props:
children(required) - Markdown stringmarkdownCustomStyles- Style overrides for HTML elements (h1, h2, p, a, codeInline, etc.)markdownContainerStyles- Styles for container div
Font
A React Font component to set your fonts.
import { Head, Font } from 'react-email';
<Head>
<Font
fontFamily="Roboto"
fallbackFontFamily="Arial, sans-serif"
webFont={{
url: "https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2",
format: "woff2"
}}
/>
</Head>Props:
fontFamily(required) - Font family namefallbackFontFamily- Fallback fontswebFont- Object withurlandformat
Supported formats:
- woff2 (recommended)
- woff
- truetype
- opentype
React Email Editor Reference
A visual rich-text editor for building email templates, built on TipTap and ProseMirror. Embed it in your app to let users compose email-ready HTML without writing code.
Table of Contents
- Installation
- CSS Setup
- Architecture
- EmailEditor Component
- Minimal Setup (Extensions Only)
- Bubble Menus
- Slash Commands
- Inspector
- Email Theming
- Email Export
- Custom Extensions
Installation
Install the editor and its peer dependencies:
npm install @react-email/editorRequires React 18+ and a bundler that supports package exports (Vite, Next.js, Webpack 5, etc.).
CSS Setup
Import the bundled default theme for the quickest start:
import '@react-email/editor/themes/default.css';This includes the default color theme and built-in UI styles for bubble menus, slash commands, and the inspector.
To import only what you need:
import '@react-email/editor/styles/bubble-menu.css';
import '@react-email/editor/styles/slash-command.css';
import '@react-email/editor/styles/inspector.css';Architecture
The editor is organized into six entry points:
| Import | Purpose |
|---|---|
@react-email/editor | EmailEditor: the all-in-one component |
@react-email/editor/core | composeReactEmail serialization, EmailNode, EmailMark, event bus, types |
@react-email/editor/extensions | StarterKit and 35+ email-aware extensions |
@react-email/editor/ui | BubbleMenu, SlashCommand, Inspector |
@react-email/editor/plugins | EmailTheming plugin |
@react-email/editor/utils | Attribute helpers, style utilities |
EmailEditor Component
The EmailEditor component from @react-email/editor is a batteries-included component that bundles StarterKit, EmailTheming, BubbleMenus, and SlashCommands. Use it when you want the full experience with minimal setup.
import { EmailEditor, type EmailEditorRef } from '@react-email/editor';
import '@react-email/editor/themes/default.css';
import { useRef } from 'react';
export function MyEditor() {
const editorRef = useRef<EmailEditorRef>(null);
const handleExport = async () => {
const { html, text } = await editorRef.current!.export();
console.log(html, text);
};
return (
<div>
<EmailEditor
ref={editorRef}
content="<p>Start typing...</p>"
theme="basic"
onReady={(editor) => console.log('Editor ready', editor)}
onChange={(editor) => console.log('Content changed')}
/>
<button onClick={handleExport}>Export HTML</button>
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
content | Content | — | Initial editor content (HTML string or TipTap JSON) |
onChange | (editor: Editor) => void | — | Called on every content change |
onUploadImage | UploadImageHandler | — | Handler for pasted/dropped images |
onReady | (editor: Editor) => void | — | Called when editor is initialized |
theme | `'basic' \ | 'minimal'` | 'basic' |
editable | boolean | true | Whether content is editable |
placeholder | string | — | Placeholder text for empty editor |
bubbleMenu | { hideWhenActiveNodes?: string[], hideWhenActiveMarks?: string[] } | — | Configure bubble menu visibility |
extensions | Extensions | — | Override the default extensions entirely |
className | string | — | CSS class for the editor container |
Ref Methods (EmailEditorRef)
| Method | Returns | Description |
|---|---|---|
export() | Promise<{ html: string; text: string }> | Export email-ready HTML and plain text |
getJSON() | JSONContent | Get editor content as TipTap JSON |
getHTML() | string | Get editor content as HTML |
editor | `Editor \ | null` |
Minimal Setup (Extensions Only)
For more control, use EditorProvider from @tiptap/react directly with StarterKit:
import { StarterKit } from '@react-email/editor/extensions';
import { EditorProvider } from '@tiptap/react';
const extensions = [StarterKit];
const content = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Start typing or edit this text.' }],
},
],
};
export function MyEditor() {
return <EditorProvider extensions={extensions} content={content} />;
}This gives you a content-editable area with all core extensions (paragraphs, headings, lists, tables, code blocks, columns, buttons, etc.) but no UI overlays.
Bubble Menus
Floating formatting toolbars that appear on text selection. Add as children of EditorProvider.
import { StarterKit } from '@react-email/editor/extensions';
import { BubbleMenu } from '@react-email/editor/ui';
import { EditorProvider } from '@tiptap/react';
import '@react-email/editor/themes/default.css';
const extensions = [StarterKit];
export function MyEditor() {
return (
<EditorProvider extensions={extensions} content={content}>
<BubbleMenu />
</EditorProvider>
);
}Available Bubble Menus
| Component | Appears when... | Controls |
|---|---|---|
BubbleMenu | Text is selected | Bold, italic, underline, strike, code, uppercase, alignment, node type, link |
BubbleMenu.LinkDefault | Cursor is on a link | Edit URL, open link, unlink |
BubbleMenu.ButtonDefault | Cursor is on a button | Edit button URL, unlink |
BubbleMenu.ImageDefault | Cursor is on an image | Edit image URL |
Exclude specific items from the default menu:
<BubbleMenu excludeItems={['strike', 'code', 'uppercase']} />When combining the text bubble menu with contextual menus for links, images, or buttons, use hideWhenActiveMarks on BubbleMenu to prevent it from appearing when a link is focused.
Slash Commands
Insert content blocks by typing / in the editor.
import { defaultSlashCommands, SlashCommand } from '@react-email/editor/ui';
<EditorProvider extensions={extensions} content={content}>
<SlashCommand items={defaultSlashCommands} />
</EditorProvider>Default Commands
| Command | Category | Description |
|---|---|---|
TEXT | Text | Plain text block |
H1, H2, H3 | Text | Headings |
BULLET_LIST | Text | Unordered list |
NUMBERED_LIST | Text | Ordered list |
QUOTE | Text | Block quote |
CODE | Text | Code snippet |
BUTTON | Layout | Clickable button |
DIVIDER | Layout | Horizontal separator |
SECTION | Layout | Content section |
TWO_COLUMNS | Layout | Two column layout |
THREE_COLUMNS | Layout | Three column layout |
FOUR_COLUMNS | Layout | Four column layout |
Cherry-pick individual commands:
import { BUTTON, H1, H2, TEXT } from '@react-email/editor/ui';
<SlashCommand items={[TEXT, H1, H2, BUTTON]} />Inspector
A contextual sidebar for editing document-level styles, node properties, and text formatting. Requires the EmailTheming plugin.
import { StarterKit } from '@react-email/editor/extensions';
import { EmailTheming } from '@react-email/editor/plugins';
import { Inspector } from '@react-email/editor/ui';
import { EditorContent, EditorContext, useEditor } from '@tiptap/react';
import '@react-email/editor/themes/default.css';
const extensions = [StarterKit, EmailTheming];
export function MyEditor() {
const editor = useEditor({ extensions, content });
return (
<EditorContext.Provider value={{ editor }}>
<div style={{ display: 'flex' }}>
<div style={{ flex: 1 }}>
<EditorContent editor={editor} />
</div>
<Inspector.Root style={{ width: 240, borderLeft: '1px solid #e5e7eb', padding: 16 }}>
<Inspector.Breadcrumb />
<Inspector.Document />
<Inspector.Node />
<Inspector.Text />
</Inspector.Root>
</div>
</EditorContext.Provider>
);
}The inspector automatically switches between document, node, and text controls based on the current selection.
Email Theming
Apply visual styles (typography, spacing, colors) to email output. Themes are resolved during composeReactEmail and inlined as style attributes.
import { StarterKit } from '@react-email/editor/extensions';
import { EmailTheming } from '@react-email/editor/plugins';
const extensions = [StarterKit, EmailTheming.configure({ theme: 'basic' })];Built-in Themes
| Theme | Description |
|---|---|
'basic' | Full styling: typography, spacing, borders, visual hierarchy. Default. |
'minimal' | Essentially no styles — blank slate for custom themes. |
Switching Themes Dynamically
const [theme, setTheme] = useState<'basic' | 'minimal'>('basic');
const extensions = [StarterKit, EmailTheming.configure({ theme })];
// Re-key EditorProvider when theme changes
<EditorProvider key={theme} extensions={extensions} content={content}>Email Export
Convert editor content to email-ready HTML and plain text.
Via EmailEditor ref
const editorRef = useRef<EmailEditorRef>(null);
const { html, text } = await editorRef.current!.export();Via composeReactEmail (lower-level)
import { composeReactEmail } from '@react-email/editor/core';
import { useCurrentEditor } from '@tiptap/react';
function ExportPanel() {
const { editor } = useCurrentEditor();
const handleExport = async () => {
if (!editor) return;
const { html, text } = await composeReactEmail({
editor,
preview: 'Inbox preview text', // optional
});
console.log(html, text);
};
return <button onClick={handleExport}>Export HTML</button>;
}The preview parameter is optional — when provided, it sets the inbox preview text in the exported HTML.
The export pipeline: 1. Reads the editor's JSON document 2. Traverses each node and mark 3. Calls renderToReactEmail() on each EmailNode and EmailMark 4. Applies theme styles via EmailTheming plugin (if configured) 5. Wraps in a base template and renders to HTML string + plain text
Custom Extensions
Create custom email-compatible nodes using EmailNode (extends TipTap's Node with renderToReactEmail()):
import { EmailNode } from '@react-email/editor/core';
import { mergeAttributes } from '@tiptap/core';
const Callout = EmailNode.create({
name: 'callout',
group: 'block',
content: 'inline*',
parseHTML() {
return [{ tag: 'div[data-callout]' }];
},
renderHTML({ HTMLAttributes }) {
return [
'div',
mergeAttributes(HTMLAttributes, {
'data-callout': '',
style: 'padding: 12px 16px; background: #f4f4f5; border-left: 3px solid #1c1c1c;',
}),
0,
];
},
renderToReactEmail({ children, style }) {
return (
<div style={{ ...style, padding: '12px 16px', backgroundColor: '#f4f4f5', borderLeft: '3px solid #1c1c1c' }}>
{children}
</div>
);
},
});
// Register it
const extensions = [StarterKit, Callout];For custom marks (inline formatting), use EmailMark from @react-email/editor/core — same pattern but for inline elements.
Internationalization (i18n) Guide
Complete guide for implementing multi-language email support with React Email using Tailwind CSS styling.
Table of Contents
- next-intl
- react-intl (FormatJS)
- react-i18next
- Message File Organization
- Best Practices
- Example: Complete Multi-locale Email
React Email officially supports three popular i18n libraries: next-intl, react-i18next, and react-intl.
next-intl
Best choice for Next.js applications with straightforward API.
Installation
npm install next-intlSetup
1. Create message files:
// messages/en.json
{
"welcome-email": {
"subject": "Welcome to Acme",
"greeting": "Hi",
"body": "Thanks for signing up! We're excited to have you on board.",
"cta": "Get Started",
"footer": "If you have questions, reply to this email."
}
}// messages/es.json
{
"welcome-email": {
"subject": "Bienvenido a Acme",
"greeting": "Hola",
"body": "¡Gracias por registrarte! Estamos emocionados de tenerte en la plataforma.",
"cta": "Comenzar",
"footer": "Si tienes preguntas, responde a este correo electrónico."
}
}// messages/fr.json
{
"welcome-email": {
"subject": "Bienvenue chez Acme",
"greeting": "Bonjour",
"body": "Merci de vous être inscrit ! Nous sommes ravis de vous accueillir.",
"cta": "Commencer",
"footer": "Si vous avez des questions, répondez à cet e-mail."
}
}2. Update email template:
import { createTranslator } from 'next-intl';
import {
Html,
Head,
Preview,
Body,
Container,
Heading,
Text,
Button,
Hr,
Tailwind,
pixelBasedPreset
} from 'react-email';
interface WelcomeEmailProps {
name: string;
verificationUrl: string;
locale: string;
}
export default async function WelcomeEmail({
name,
verificationUrl,
locale
}: WelcomeEmailProps) {
const t = createTranslator({
messages: await import(`../messages/${locale}.json`),
namespace: 'welcome-email',
locale
});
return (
<Html lang={locale}>
<Tailwind config={{ presets: [pixelBasedPreset] }}>
<Head />
<Body className="bg-gray-100 font-sans">
<Preview>{t('subject')}</Preview>
<Container className="mx-auto py-10 px-5 max-w-xl">
<Heading className="text-2xl font-bold text-gray-800">
{t('subject')}
</Heading>
<Text className="text-base leading-7 text-gray-800 my-4">
{t('greeting')} {name},
</Text>
<Text className="text-base leading-7 text-gray-800 my-4">
{t('body')}
</Text>
<Button
href={verificationUrl}
className="bg-blue-600 text-white px-5 py-3 rounded block text-center no-underline box-border"
>
{t('cta')}
</Button>
<Hr className="border-solid border-gray-200 my-5" />
<Text className="text-sm text-gray-500">
{t('footer')}
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
}
// Preview props
WelcomeEmail.PreviewProps = {
name: 'John',
verificationUrl: 'https://example.com/verify',
locale: 'en'
} as WelcomeEmailProps;3. Send with locale:
await resend.emails.send({
from: 'Acme <onboarding@resend.dev>',
to: ['user@example.com'],
subject: 'Welcome',
react: <WelcomeEmail name="Jean" verificationUrl="..." locale="fr" />
});react-intl (FormatJS)
Good choice for complex formatting needs (plurals, dates, numbers).
Installation
npm install react-intlSetup
1. Create message files:
// messages/en/welcome-email.json
{
"header": "Welcome to Acme",
"greeting": "Hi",
"body": "Thanks for signing up!",
"cta": "Get Started",
"itemCount": "{count, plural, one {# item} other {# items}}"
}2. Use in email:
import { createIntl } from 'react-intl';
import {
Html,
Body,
Container,
Text,
Button,
Tailwind,
pixelBasedPreset
} from 'react-email';
interface WelcomeEmailProps {
name: string;
locale: string;
itemCount?: number;
}
export default async function WelcomeEmail({
name,
locale,
itemCount = 1
}: WelcomeEmailProps) {
const { formatMessage } = createIntl({
locale,
messages: await import(`../messages/${locale}/welcome-email.json`)
});
return (
<Html lang={locale}>
<Tailwind config={{ presets: [pixelBasedPreset] }}>
<Body className="bg-gray-100 font-sans">
<Container className="mx-auto p-5 max-w-xl">
<Text className="text-base text-gray-800">
{formatMessage({ id: 'greeting' })} {name},
</Text>
<Text className="text-base text-gray-800">
{formatMessage({ id: 'body' })}
</Text>
<Text className="text-base text-gray-800">
{formatMessage({ id: 'itemCount' }, { count: itemCount })}
</Text>
<Button
href="https://example.com"
className="bg-blue-600 text-white px-5 py-3 rounded box-border"
>
{formatMessage({ id: 'cta' })}
</Button>
</Container>
</Body>
</Tailwind>
</Html>
);
}react-i18next
Best for non-Next.js applications or when you need more control.
Installation
npm install react-i18next i18next i18next-resources-to-backendSetup
1. Configure i18next:
// i18n.js
import i18next from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import { initReactI18next } from 'react-i18next';
i18next
.use(initReactI18next)
.use(resourcesToBackend((language, namespace) =>
import(`./messages/${language}/${namespace}.json`)
))
.init({
supportedLngs: ['en', 'es', 'fr', 'de'],
fallbackLng: 'en',
lng: undefined,
preload: ['en', 'es', 'fr', 'de']
});
export { i18next };2. Create translation helper:
// get-t.js
import { i18next } from './i18n';
export async function getT(namespace, locale) {
if (locale && i18next.resolvedLanguage !== locale) {
await i18next.changeLanguage(locale);
}
if (namespace && !i18next.hasLoadedNamespace(namespace)) {
await i18next.loadNamespaces(namespace);
}
return {
t: i18next.getFixedT(
locale ?? i18next.resolvedLanguage,
Array.isArray(namespace) ? namespace[0] : namespace
),
i18n: i18next
};
}3. Create message files:
// messages/en/welcome-email.json
{
"subject": "Welcome to Acme",
"greeting": "Hi",
"body": "Thanks for signing up!",
"cta": "Get Started"
}// messages/es/welcome-email.json
{
"subject": "Bienvenido a Acme",
"greeting": "Hola",
"body": "¡Gracias por registrarte!",
"cta": "Comenzar"
}4. Use in email template:
import { getT } from '../get-t';
import {
Html,
Body,
Container,
Heading,
Text,
Button,
Tailwind,
pixelBasedPreset
} from 'react-email';
interface WelcomeEmailProps {
name: string;
locale: string;
}
export default async function WelcomeEmail({ name, locale }: WelcomeEmailProps) {
const { t } = await getT('welcome-email', locale);
return (
<Html lang={locale}>
<Tailwind config={{ presets: [pixelBasedPreset] }}>
<Body className="bg-gray-100 font-sans">
<Container className="mx-auto p-5 max-w-xl">
<Heading className="text-2xl font-bold text-gray-800">
{t('subject')}
</Heading>
<Text className="text-base text-gray-800">
{t('greeting')} {name},
</Text>
<Text className="text-base text-gray-800">
{t('body')}
</Text>
<Button
href="https://example.com"
className="bg-blue-600 text-white px-5 py-3 rounded box-border"
>
{t('cta')}
</Button>
</Container>
</Body>
</Tailwind>
</Html>
);
}Message File Organization
By Namespace (Recommended)
Organize translations by email template:
messages/
├── en.json # All English translations
│ ├── welcome-email
│ ├── password-reset
│ └── order-confirmation
├── es.json # All Spanish translations
└── fr.json # All French translationsOr organize by template with separate files:
messages/
├── en/
│ ├── welcome-email.json
│ ├── password-reset.json
│ └── order-confirmation.json
├── es/
│ ├── welcome-email.json
│ ├── password-reset.json
│ └── order-confirmation.json
└── fr/
├── welcome-email.json
├── password-reset.json
└── order-confirmation.jsonTranslation Keys
Use descriptive, hierarchical keys:
{
"welcome-email": {
"subject": "Welcome!",
"preview": "Get started with your account",
"header": {
"title": "Welcome to Acme",
"subtitle": "We're glad you're here"
},
"body": {
"greeting": "Hi",
"intro": "Thanks for signing up!",
"next-steps": "Here's how to get started:"
},
"cta": {
"primary": "Get Started",
"secondary": "Learn More"
},
"footer": {
"help": "Need help? Reply to this email",
"unsubscribe": "Unsubscribe from these emails"
}
}
}Best Practices
1. Always Pass Locale
Make locale a required prop:
interface EmailProps {
locale: string;
// other props...
}2. Set HTML Lang Attribute
<Html lang={locale}>3. Support RTL Languages
For Arabic, Hebrew, etc.:
const isRTL = ['ar', 'he', 'fa'].includes(locale);
<Html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>4. Fallback Values
Provide fallback translations:
const t = createTranslator({
messages: await import(`../messages/${locale}.json`).catch(() =>
import('../messages/en.json')
),
locale,
namespace: 'welcome-email'
});5. Test All Locales
Test email rendering for each supported locale:
WelcomeEmail.PreviewProps = {
name: 'Test User',
locale: 'en' // Change to test different locales
} as WelcomeEmailProps;6. Keep Keys Consistent
Use the same translation keys across all locale files:
// ✅ Good
// en.json: { "cta": "Get Started" }
// es.json: { "cta": "Comenzar" }
// ❌ Bad
// en.json: { "button": "Get Started" }
// es.json: { "cta": "Comenzar" }7. Handle Missing Translations
Set up fallback behavior:
// With next-intl
const t = createTranslator({
messages,
locale,
namespace: 'welcome-email',
onError: (error) => {
console.warn('Translation missing:', error);
}
});8. Subject Line Translation
Don't forget to translate email subjects:
const t = createTranslator({...});
await resend.emails.send({
from: 'Acme <onboarding@resend.dev>',
to: [user.email],
subject: t('subject'), // ✅ Translated subject
react: <WelcomeEmail {...props} />
});9. Format Consistency
Maintain consistent formatting across locales:
- Date formats (MM/DD/YYYY vs DD/MM/YYYY)
- Time formats (12h vs 24h)
- Number separators (1,234.56 vs 1.234,56)
- Currency symbols and placement ($100 vs 100$)
Use Intl APIs for automatic locale-specific formatting.
Example: Complete Multi-locale Email
import { createTranslator } from 'next-intl';
import {
Html,
Head,
Preview,
Body,
Container,
Section,
Heading,
Text,
Button,
Hr,
Tailwind,
pixelBasedPreset
} from 'react-email';
interface OrderConfirmationProps {
orderNumber: string;
total: number;
currency: string;
locale: string;
orderDate: Date;
}
export default async function OrderConfirmation({
orderNumber,
total,
currency,
locale,
orderDate
}: OrderConfirmationProps) {
const t = createTranslator({
messages: await import(`../messages/${locale}.json`),
namespace: 'order-confirmation',
locale
});
const isRTL = ['ar', 'he'].includes(locale);
const currencyFormatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency
});
const dateFormatter = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
});
return (
<Html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>
<Tailwind config={{ presets: [pixelBasedPreset] }}>
<Head />
<Body className="bg-gray-100 font-sans">
<Preview>{t('preview')}</Preview>
<Container className="mx-auto py-10 px-5 max-w-xl">
<Heading className="text-2xl font-bold text-gray-800">
{t('title')}
</Heading>
<Text className="text-base text-gray-800 my-2">
{t('order-number')}: {orderNumber}
</Text>
<Text className="text-base text-gray-800 my-2">
{t('order-date')}: {dateFormatter.format(orderDate)}
</Text>
<Section className="bg-white p-5 rounded my-4">
<Text className="text-xl font-bold text-gray-800">
{t('total')}: {currencyFormatter.format(total)}
</Text>
</Section>
<Button
href={`https://example.com/orders/${orderNumber}`}
className="bg-blue-600 text-white px-5 py-3 rounded block text-center no-underline my-5 box-border"
>
{t('view-order')}
</Button>
<Hr className="border-solid border-gray-200 my-5" />
<Text className="text-sm text-gray-500">
{t('footer')}
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
}With message files:
// messages/en.json
{
"order-confirmation": {
"preview": "Your order has been confirmed",
"title": "Order Confirmed",
"order-number": "Order number",
"order-date": "Order date",
"total": "Total",
"view-order": "View Order",
"footer": "Thank you for your purchase!"
}
}// messages/es.json
{
"order-confirmation": {
"preview": "Tu pedido ha sido confirmado",
"title": "Pedido Confirmado",
"order-number": "Número de pedido",
"order-date": "Fecha del pedido",
"total": "Total",
"view-order": "Ver Pedido",
"footer": "¡Gracias por tu compra!"
}
}Common Email Patterns
Real-world examples of common email templates using React Email with Tailwind CSS styling.
Table of Contents
- Password Reset Email
- Order Confirmation with Product List
- Notification Email with Code Block
- Multi-Column Newsletter
- Team Invitation Email
Password Reset Email
import {
Html,
Head,
Preview,
Body,
Container,
Heading,
Text,
Button,
Hr,
Tailwind,
pixelBasedPreset
} from 'react-email';
interface PasswordResetProps {
resetUrl: string;
email: string;
expiryHours?: number;
}
export default function PasswordReset({ resetUrl, email, expiryHours = 1 }: PasswordResetProps) {
return (
<Html lang="en">
<Tailwind config={{ presets: [pixelBasedPreset] }}>
<Head />
<Body className="bg-gray-100 font-sans">
<Preview>Reset your password - Action required</Preview>
<Container className="mx-auto py-10 px-5 max-w-xl bg-white">
<Heading className="text-2xl font-bold text-gray-800 mb-5">
Reset Your Password
</Heading>
<Text className="text-base leading-7 text-gray-800 my-4">
A password reset was requested for your account: <strong>{email}</strong>
</Text>
<Text className="text-base leading-7 text-gray-800 my-4">
Click the button below to reset your password. This link expires in {expiryHours} hour{expiryHours > 1 ? 's' : ''}.
</Text>
<Button
href={resetUrl}
className="bg-red-600 text-white px-7 py-3.5 rounded block text-center font-bold my-6 no-underline box-border"
>
Reset Password
</Button>
<Hr className="border-solid border-gray-200 my-6" />
<Text className="text-sm text-gray-500 leading-5 my-2">
If you didn't request this, please ignore this email. Your password will remain unchanged.
</Text>
<Text className="text-sm text-gray-500 leading-5 my-2">
For security, this link will only work once.
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
}
PasswordReset.PreviewProps = {
resetUrl: 'https://example.com/reset/abc123',
email: 'user@example.com',
expiryHours: 1
} as PasswordResetProps;Order Confirmation with Product List
import {
Html,
Head,
Preview,
Body,
Container,
Section,
Row,
Column,
Heading,
Text,
Img,
Hr,
Tailwind,
pixelBasedPreset
} from 'react-email';
interface Product {
name: string;
price: number;
quantity: number;
image: string;
sku?: string;
}
interface OrderConfirmationProps {
orderNumber: string;
orderDate: Date;
items: Product[];
subtotal: number;
shipping: number;
tax: number;
total: number;
shippingAddress: {
name: string;
street: string;
city: string;
state: string;
zip: string;
country: string;
};
}
export default function OrderConfirmation({
orderNumber,
orderDate,
items,
subtotal,
shipping,
tax,
total,
shippingAddress
}: OrderConfirmationProps) {
return (
<Html lang="en">
<Tailwind config={{ presets: [pixelBasedPreset] }}>
<Head />
<Body className="bg-gray-100 font-sans">
<Preview>Order #{orderNumber} confirmed - Thank you for your purchase!</Preview>
<Container className="mx-auto py-10 px-5 max-w-xl">
<Heading className="text-3xl font-bold text-gray-800 mb-2">
Order Confirmed
</Heading>
<Text className="text-base text-gray-500 mb-6">Thank you for your order!</Text>
<Section className="bg-gray-50 p-4 rounded mb-6">
<Row>
<Column>
<Text className="text-xs text-gray-500 uppercase mb-1">Order Number</Text>
<Text className="text-base font-bold text-gray-800 m-0">#{orderNumber}</Text>
</Column>
<Column>
<Text className="text-xs text-gray-500 uppercase mb-1">Order Date</Text>
<Text className="text-base font-bold text-gray-800 m-0">{orderDate.toLocaleDateString()}</Text>
</Column>
</Row>
</Section>
<Hr className="border-solid border-gray-200 my-6" />
<Heading as="h2" className="text-xl font-bold text-gray-800 my-4">
Order Items
</Heading>
{items.map((item, index) => (
<Section key={index} className="mb-4">
<Row>
<Column className="w-20 align-top">
<Img
src={item.image}
alt={item.name}
width="80"
height="80"
className="rounded border border-solid border-gray-200"
/>
</Column>
<Column className="align-top pl-4">
<Text className="text-base font-bold text-gray-800 m-0 mb-1">{item.name}</Text>
{item.sku && <Text className="text-sm text-gray-400 m-0 mb-2">SKU: {item.sku}</Text>}
<Text className="text-sm text-gray-500 m-0">
Quantity: {item.quantity} × ${item.price.toFixed(2)}
</Text>
</Column>
<Column className="w-24 text-right align-top">
<Text className="text-base font-bold text-gray-800 m-0">
${(item.quantity * item.price).toFixed(2)}
</Text>
</Column>
</Row>
</Section>
))}
<Hr className="border-solid border-gray-200 my-6" />
<Section className="mt-6">
<Row>
<Column><Text className="text-sm text-gray-500 my-2">Subtotal</Text></Column>
<Column className="text-right">
<Text className="text-sm text-gray-800 my-2">${subtotal.toFixed(2)}</Text>
</Column>
</Row>
<Row>
<Column><Text className="text-sm text-gray-500 my-2">Shipping</Text></Column>
<Column className="text-right">
<Text className="text-sm text-gray-800 my-2">${shipping.toFixed(2)}</Text>
</Column>
</Row>
<Row>
<Column><Text className="text-sm text-gray-500 my-2">Tax</Text></Column>
<Column className="text-right">
<Text className="text-sm text-gray-800 my-2">${tax.toFixed(2)}</Text>
</Column>
</Row>
<Hr className="border-solid border-gray-200 my-3" />
<Row>
<Column><Text className="text-lg font-bold text-gray-800 my-2">Total</Text></Column>
<Column className="text-right">
<Text className="text-lg font-bold text-gray-800 my-2">${total.toFixed(2)}</Text>
</Column>
</Row>
</Section>
<Hr className="border-solid border-gray-200 my-6" />
<Heading as="h2" className="text-xl font-bold text-gray-800 my-4">
Shipping Address
</Heading>
<Section className="bg-gray-50 p-4 rounded">
<Text className="text-sm text-gray-800 my-1">{shippingAddress.name}</Text>
<Text className="text-sm text-gray-800 my-1">{shippingAddress.street}</Text>
<Text className="text-sm text-gray-800 my-1">
{shippingAddress.city}, {shippingAddress.state} {shippingAddress.zip}
</Text>
<Text className="text-sm text-gray-800 my-1">{shippingAddress.country}</Text>
</Section>
<Text className="text-sm text-gray-500 mt-8">
Questions about your order? Reply to this email and we'll help you out.
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
}
OrderConfirmation.PreviewProps = {
orderNumber: '10234',
orderDate: new Date(),
items: [
{
name: 'Vintage Macintosh',
price: 499.00,
quantity: 1,
image: 'https://via.placeholder.com/80',
sku: 'MAC-001'
},
{
name: 'Mechanical Keyboard',
price: 149.99,
quantity: 2,
image: 'https://via.placeholder.com/80',
sku: 'KEY-042'
}
],
subtotal: 798.98,
shipping: 15.00,
tax: 69.42,
total: 883.40,
shippingAddress: {
name: 'John Doe',
street: '123 Main St',
city: 'San Francisco',
state: 'CA',
zip: '94102',
country: 'USA'
}
} as OrderConfirmationProps;Notification Email with Code Block
import {
Html,
Head,
Preview,
Body,
Container,
Section,
Heading,
Text,
CodeBlock,
dracula,
Hr,
Link,
Tailwind,
pixelBasedPreset
} from 'react-email';
interface NotificationProps {
title: string;
message: string;
severity: 'info' | 'warning' | 'error' | 'success';
timestamp: Date;
logData?: string;
actionUrl?: string;
actionLabel?: string;
}
export default function Notification({
title,
message,
severity,
timestamp,
logData,
actionUrl,
actionLabel = 'View Details'
}: NotificationProps) {
const severityColors = {
info: 'bg-sky-500',
warning: 'bg-amber-500',
error: 'bg-red-500',
success: 'bg-green-500'
};
const severityBtnColors = {
info: 'bg-sky-500',
warning: 'bg-amber-500',
error: 'bg-red-500',
success: 'bg-green-500'
};
return (
<Html lang="en">
<Tailwind config={{ presets: [pixelBasedPreset] }}>
<Head />
<Body className="bg-gray-100 font-mono">
<Preview>{title} - {severity}</Preview>
<Container className="mx-auto max-w-xl bg-white border border-solid border-gray-200 rounded overflow-hidden">
<Section className={`h-1 w-full ${severityColors[severity]}`} />
<Heading className="text-2xl font-bold text-gray-800 mx-6 mt-6 mb-4">
{title}
</Heading>
<Text className={`inline-block px-3 py-1 text-xs font-bold text-white rounded-full mx-6 mb-4 ${severityBtnColors[severity]}`}>
{severity.toUpperCase()}
</Text>
<Text className="text-base leading-6 text-gray-800 mx-6 mb-4">
{message}
</Text>
<Text className="text-sm text-gray-500 mx-6 mb-6">
{new Date(timestamp).toLocaleString('en-US', {
dateStyle: 'long',
timeStyle: 'short'
})}
</Text>
{logData && (
<>
<Hr className="border-solid border-gray-200 my-6" />
<Heading as="h2" className="text-lg font-bold text-gray-800 mx-6 my-4">
Log Details
</Heading>
<Section className="overflow-auto mx-6">
<CodeBlock
code={logData}
language="json"
theme={dracula}
/>
</Section>
</>
)}
{actionUrl && (
<>
<Hr className="border-solid border-gray-200 my-6" />
<Link
href={actionUrl}
className={`inline-block px-6 py-3 text-base font-bold text-white rounded no-underline mx-6 mb-6 ${severityBtnColors[severity]}`}
>
{actionLabel}
</Link>
</>
)}
<Hr className="border-solid border-gray-200 my-6" />
<Text className="text-xs text-gray-500 mx-6 mb-6">
This is an automated notification. Please do not reply to this email.
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
}
Notification.PreviewProps = {
title: 'Deployment Failed',
message: 'The deployment to production environment has failed. Please review the logs and take corrective action.',
severity: 'error',
timestamp: new Date(),
logData: `{
"error": "Build failed",
"exit_code": 1,
"duration": "2m 34s",
"commit": "abc123def"
}`,
actionUrl: 'https://example.com/deployments/123',
actionLabel: 'View Deployment'
} as NotificationProps;Multi-Column Newsletter
import {
Html,
Head,
Preview,
Body,
Container,
Section,
Row,
Column,
Heading,
Text,
Img,
Button,
Hr,
Link,
Tailwind,
pixelBasedPreset
} from 'react-email';
interface Article {
title: string;
excerpt: string;
image: string;
url: string;
author: string;
date: string;
}
interface NewsletterProps {
articles: Article[];
unsubscribeUrl: string;
}
export default function Newsletter({ articles, unsubscribeUrl }: NewsletterProps) {
return (
<Html lang="en">
<Tailwind config={{ presets: [pixelBasedPreset] }}>
<Head />
<Body className="bg-white font-sans">
<Preview>Your weekly roundup of the latest articles</Preview>
<Container className="mx-auto max-w-xl">
{/* Header */}
<Section className="pt-10 px-5 pb-5 text-center">
<Img
src="https://via.placeholder.com/150x50?text=Logo"
alt="Company Logo"
width="150"
height="50"
/>
</Section>
<Heading className="text-3xl font-bold text-gray-900 mx-5 mb-4 text-center">
This Week's Highlights
</Heading>
<Text className="text-base leading-6 text-gray-500 mx-5 mb-6 text-center">
Here are the top articles from this week. Enjoy your reading!
</Text>
<Hr className="border-solid border-gray-200 mx-5 my-8" />
{/* Featured Article */}
{articles[0] && (
<Section className="px-5">
<Img
src={articles[0].image}
alt={articles[0].title}
width="600"
className="w-full rounded-lg mb-4"
/>
<Heading as="h2" className="text-2xl font-bold text-gray-900 my-4">
{articles[0].title}
</Heading>
<Text className="text-base leading-6 text-gray-500 my-4">
{articles[0].excerpt}
</Text>
<Text className="text-sm text-gray-400 my-2">
By {articles[0].author} • {articles[0].date}
</Text>
<Button
href={articles[0].url}
className="bg-blue-600 text-white px-6 py-3 rounded font-bold inline-block no-underline box-border"
>
Read More
</Button>
</Section>
)}
<Hr className="border-solid border-gray-200 mx-5 my-8" />
{/* Two-Column Articles */}
{articles.slice(1, 5).length > 0 && (
<>
<Heading as="h2" className="text-2xl font-bold text-gray-900 mx-5 my-4">
More From This Week
</Heading>
{Array.from({ length: Math.ceil(articles.slice(1, 5).length / 2) }).map((_, rowIndex) => {
const leftArticle = articles[1 + rowIndex * 2];
const rightArticle = articles[2 + rowIndex * 2];
return (
<Section key={rowIndex} className="px-5 mb-6">
<Row>
{leftArticle && (
<Column className="w-1/2 align-top px-1">
<Img
src={leftArticle.image}
alt={leftArticle.title}
width="280"
className="w-full rounded mb-3"
/>
<Heading as="h3" className="text-lg font-bold text-gray-900 my-3">
{leftArticle.title}
</Heading>
<Text className="text-sm leading-5 text-gray-500 my-2">
{leftArticle.excerpt}
</Text>
<Link href={leftArticle.url} className="text-sm text-blue-600 no-underline font-semibold">
Read article →
</Link>
</Column>
)}
{rightArticle && (
<Column className="w-1/2 align-top px-1">
<Img
src={rightArticle.image}
alt={rightArticle.title}
width="280"
className="w-full rounded mb-3"
/>
<Heading as="h3" className="text-lg font-bold text-gray-900 my-3">
{rightArticle.title}
</Heading>
<Text className="text-sm leading-5 text-gray-500 my-2">
{rightArticle.excerpt}
</Text>
<Link href={rightArticle.url} className="text-sm text-blue-600 no-underline font-semibold">
Read article →
</Link>
</Column>
)}
</Row>
</Section>
);
})}
</>
)}
<Hr className="border-solid border-gray-200 mx-5 my-8" />
{/* Footer */}
<Section className="bg-gray-50 p-8 mt-8 text-center">
<Text className="text-sm text-gray-500 my-2">
You're receiving this because you subscribed to our newsletter.
</Text>
<Link href={unsubscribeUrl} className="text-sm text-blue-600 underline block my-2">
Unsubscribe from this list
</Link>
<Text className="text-sm text-gray-500 my-2">
© 2026 Company Name. All rights reserved.
</Text>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
}
Newsletter.PreviewProps = {
articles: [
{
title: 'The Future of Web Development in 2026',
excerpt: 'Exploring the latest trends and technologies shaping modern web development.',
image: 'https://via.placeholder.com/600x300',
url: 'https://example.com/article-1',
author: 'Jane Doe',
date: 'Jan 15, 2026'
},
{
title: 'React Server Components Explained',
excerpt: 'A deep dive into React Server Components and their benefits.',
image: 'https://via.placeholder.com/280x140',
url: 'https://example.com/article-2',
author: 'John Smith',
date: 'Jan 14, 2026'
},
{
title: 'Building Accessible Web Apps',
excerpt: 'Best practices for creating inclusive digital experiences.',
image: 'https://via.placeholder.com/280x140',
url: 'https://example.com/article-3',
author: 'Sarah Johnson',
date: 'Jan 13, 2026'
}
],
unsubscribeUrl: 'https://example.com/unsubscribe'
} as NewsletterProps;Team Invitation Email
import {
Html,
Head,
Preview,
Body,
Container,
Section,
Heading,
Text,
Button,
Hr,
Tailwind,
pixelBasedPreset
} from 'react-email';
interface TeamInvitationProps {
inviterName: string;
inviterEmail: string;
teamName: string;
role: string;
inviteUrl: string;
expiryDays: number;
}
export default function TeamInvitation({
inviterName,
inviterEmail,
teamName,
role,
inviteUrl,
expiryDays
}: TeamInvitationProps) {
return (
<Html lang="en">
<Tailwind config={{ presets: [pixelBasedPreset] }}>
<Head />
<Body className="bg-gray-100 font-sans">
<Preview>You've been invited to join {teamName}</Preview>
<Container className="mx-auto py-10 px-5 max-w-xl bg-white">
<Heading className="text-3xl font-bold text-gray-800 text-center mb-6">
You're Invited!
</Heading>
<Text className="text-base leading-7 text-gray-800 my-4">
<strong>{inviterName}</strong> ({inviterEmail}) has invited you to join the{' '}
<strong>{teamName}</strong> team.
</Text>
<Section className="bg-gray-50 p-5 rounded border border-solid border-gray-200 my-6">
<Text className="text-xs text-gray-500 uppercase font-bold mb-2">Role</Text>
<Text className="text-lg font-bold text-gray-800 m-0">{role}</Text>
</Section>
<Text className="text-base leading-7 text-gray-800 my-4">
Click the button below to accept the invitation and get started.
</Text>
<Button
href={inviteUrl}
className="bg-green-600 text-white px-7 py-3.5 rounded block text-center font-bold text-base my-6 no-underline box-border"
>
Accept Invitation
</Button>
<Hr className="border-solid border-gray-200 my-6" />
<Text className="text-sm text-gray-500 leading-5 my-2">
This invitation will expire in {expiryDays} day{expiryDays > 1 ? 's' : ''}.
</Text>
<Text className="text-sm text-gray-500 leading-5 my-2">
If you weren't expecting this invitation, you can safely ignore this email.
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
}
TeamInvitation.PreviewProps = {
inviterName: 'John Doe',
inviterEmail: 'john@example.com',
teamName: 'Acme Corp Engineering',
role: 'Developer',
inviteUrl: 'https://example.com/invite/abc123',
expiryDays: 7
} as TeamInvitationProps;These patterns demonstrate:
- Tailwind CSS utility classes for styling
- Proper component usage with
pixelBasedPreset - TypeScript typing
- Preview props for testing
- Responsive layouts
- Common email scenarios
Sending Guide
General guidelines for sending emails with React Email.
Important: Use verified domains in from addresses. Ask the user for the verified domain and use it in the from address. If the user does not have a verified domain, ask them to verify one with their email service provider.
Send with Resend (Recommended)
When you have access to the Resend MCP tool:
import { render } from 'react-email';
import { WelcomeEmail } from './emails/welcome';
// Render to HTML
const html = await render(
<WelcomeEmail name="John" verificationUrl="https://example.com/verify" />
);
// Create plain text version
const text = await render(<WelcomeEmail name="John" verificationUrl="https://example.com/verify" />, { plainText: true });
// Use Resend MCP send-email tool with:
// - to: recipient@example.com
// - subject: Welcome to Acme
// - html: html
// - text: textIf no MCP tool is available, you can use the Resend SDK for Node.js to send the email, which can accept React components directly:
import { Resend } from 'resend';
import { WelcomeEmail } from './emails/welcome';
const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: 'Acme <onboarding@resend.dev>',
to: ['user@example.com'],
subject: 'Welcome to Acme',
react: <WelcomeEmail name="John" verificationUrl="https://example.com/verify" />
});
if (error) {
console.error('Failed to send:', error);
}The Node SDK automatically handles the plain-text rendering and HTML rendering for you.
Send as a Template to Resend
If preferred, you can upload the email as a template to Resend, which can be used to send emails with the Resend SDK for Node.js:
npx react-email@latest resend setupThis will require the user to provide a Resend API key in the terminal.
Once configured, the user can select a template to send using the UI in the "Resend" tab using the "Upload" button or the "Bulk Upload" button to upload multiple emails at once.
If using a template when sending with the Resend SDK for Node.js, the user can pass the template ID to the send method:
await resend.emails.send({
from: 'Acme <onboarding@resend.dev>',
to: ['user@example.com'],
subject: 'Welcome to Acme',
template: {
id: '1245-1256-1234-1234',
}
});Send with Other Providers
Nodemailer:
import { render } from 'react-email';
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: 'smtp.example.com',
port: 587,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }
});
const html = await render(<WelcomeEmail name="John" verificationUrl="https://example.com/verify" />);
await transporter.sendMail({
from: 'noreply@example.com',
to: 'user@example.com',
subject: 'Welcome',
html
});Mailgun:
import { render } from 'react-email';
import FormData from 'form-data';
import Mailgun from 'mailgun.js';
import { WelcomeEmail } from './emails/welcome';
const mailgun = new Mailgun(FormData);
const client = mailgun.client({
username: 'api',
key: process.env.MAILGUN_API_KEY,
});
const html = await render(<WelcomeEmail name="John" verificationUrl="https://example.com/verify" />);
await client.messages.create(process.env.MAILGUN_DOMAIN, {
from: 'noreply@example.com',
to: ['user@example.com'],
subject: 'Welcome',
html,
});SendGrid:
import { render } from 'react-email';
import sgMail from '@sendgrid/mail';
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const html = await render(<WelcomeEmail name="John" verificationUrl="https://example.com/verify" />);
await sgMail.send({
to: 'user@example.com',
from: 'noreply@example.com',
subject: 'Welcome',
html
});Styling Guide
Comprehensive styling reference for React Email templates.
Styling Approach
Use the Tailwind component for styling if the project uses Tailwind CSS. Otherwise, use inline styles.
import { Tailwind, pixelBasedPreset } from 'react-email';
<Tailwind
config={{
presets: [pixelBasedPreset],
theme: {
extend: {
colors: {
brand: '#007bff',
},
},
},
}}
>
{/* Email content */}
</Tailwind>pixelBasedPreset
Email clients don't support rem units. Always use pixelBasedPreset in your Tailwind configuration to convert rem-based utilities to pixels:
import { pixelBasedPreset } from 'react-email';
<Tailwind config={{ presets: [pixelBasedPreset] }}>Email Client Limitations
Email clients have significant CSS restrictions. Follow these rules:
Unsupported Features
- SVG/WEBP images - Use PNG or JPEG only
- Flexbox/Grid - Use
Row/Columncomponents or tables - Media queries -
sm:,md:,lg:,xl:prefixes don't work - Theme selectors -
dark:,light:prefixes don't work - rem units - Use
pixelBasedPresetfor pixel conversion
Border Handling
Always specify border style and reset other sides when needed:
// Correct - specify border style
<div className="border-solid border border-gray-300" />
// Correct - single side border with reset
<div className="border-none border-l border-solid border-l-gray-300" />
// Incorrect - missing border style
<div className="border border-gray-300" />Component Structure
Head Placement
Always define <Head /> inside <Tailwind> when using Tailwind CSS:
<Html>
<Tailwind config={{ presets: [pixelBasedPreset] }}>
<Head />
<Body>...</Body>
</Tailwind>
</Html>PreviewProps
Only include props that the component actually uses:
const Email = ({ source }: { source: string }) => {
return (
<div>
<a href={source}>Click here</a>
</div>
);
};
Email.PreviewProps = {
source: "https://example.com",
};Default Layout Structure
Body
<Body className="font-sans py-10 bg-gray-100">Container
White background, centered, left-aligned content:
<Container className="mx-auto bg-white p-6 rounded">Footer
Include physical address, unsubscribe link, current year:
<Section className="text-center text-gray-500 text-sm">
<Text className="m-0">123 Main St, City, State 12345</Text>
<Text className="m-0">© {new Date().getFullYear()} Company Name</Text>
<Link href={unsubscribeUrl}>Unsubscribe</Link>
</Section>Typography
Titles
Bold, larger font, larger margins:
<Heading className="text-2xl font-bold text-gray-900 mb-4">Paragraphs
Regular weight, smaller font, smaller margins:
<Text className="text-base text-gray-700 mb-3">Hierarchy
Use consistent spacing that respects content hierarchy. Larger margins for headings, smaller for body text.
Images
- Only include if user requests
- Content images: use responsive sizing (
w-full,h-auto) - Small icons (24-48px): fixed dimensions are acceptable
- Never distort user-provided images
- Never create SVG images
- Always use absolute URLs
- Include
alttext for accessibility
<Img
src="https://example.com/image.png"
alt="Description"
className="w-full h-auto"
/>Buttons
Always use box-border to prevent padding overflow:
<Button
href="https://example.com"
className="bg-blue-600 text-white px-5 py-3 rounded box-border block text-center no-underline"
>
Click Here
</Button>Layout
Mobile-First
Always design for mobile by default:
- Use stacked layouts that work on all screen sizes
- Max-width around 600px for main container
- Remove default spacing/margins/padding between list items
Multi-Column
Use Row and Column components instead of flexbox/grid:
<Row>
<Column className="w-1/2">Left content</Column>
<Column className="w-1/2">Right content</Column>
</Row>Dark Mode
When requested, use dark backgrounds:
- Container: black (
#000) - Background: dark gray (
#151516)
<Body className="bg-[#151516]">
<Container className="bg-black text-white">Colors and Brand Consistency
Gathering Brand Colors
Before creating emails, collect these colors from the user:
- Primary: Main brand color for buttons, links, key accents
- Secondary: Supporting color for borders, backgrounds, less prominent elements
- Text: Main body text color (suggest
#1a1a1afor light backgrounds) - Text muted: Secondary text like captions, footers (suggest
#6b7280) - Background: Email body background (suggest
#f4f4f5) - Surface: Container/card background (typically
#ffffff)
Tailwind Configuration File
Create a centralized Tailwind config file that all email templates import. Using satisfies TailwindConfig provides intellisense support for all configuration options:
// emails/tailwind.config.ts
import { pixelBasedPreset, type TailwindConfig } from 'react-email';
export default {
presets: [pixelBasedPreset],
theme: {
extend: {
colors: {
brand: {
primary: '#007bff',
secondary: '#6c757d',
},
},
},
},
} satisfies TailwindConfig;
// For non-Tailwind brand assets (optional)
export const brandAssets = {
logo: {
src: 'https://example.com/logo.png',
alt: 'Company Name',
width: 120,
},
};Using Tailwind Config
Import the shared config in every email template:
import tailwindConfig, { brandAssets } from './tailwind.config';
<Tailwind config={tailwindConfig}>
<Body className="bg-gray-100 font-sans">
<Container className="bg-white p-6">
<Img src={brandAssets.logo.src} alt={brandAssets.logo.alt} width={brandAssets.logo.width} />
<Button className="bg-brand-primary text-white">Action</Button>
</Container>
</Body>
</Tailwind>Maintaining Consistency
- Always use the brand config - Never hardcode colors in individual templates
- Update config, not templates - When colors change, update
tailwind.config.tsonly - Use semantic names -
bg-brand-primarynotbg-[#007bff] - Ensure contrast - Test that text is readable against backgrounds (WCAG AA: 4.5:1 ratio)
Asset Locations
Direct users to place brand assets in appropriate locations:
- Logo and images: Host on a CDN or public URL. For local development, place in
emails/static/. - Custom fonts: Use the
Fontcomponent with a web font URL (Google Fonts, Adobe Fonts, or self-hosted).
Example prompt for gathering brand info:
"Before I create your email template, I need some brand information to ensure consistency. Could you provide:
1. Your primary brand color (hex code, e.g., #007bff)
2. Your logo URL (must be a publicly accessible PNG or JPEG)
3. Any secondary colors you'd like to use
4. Style preference (modern/minimal or classic/traditional)"
Best Practices
1. Make templates unique - Not generic, tailored to user's request 2. Test across clients - Gmail, Outlook, Apple Mail, Yahoo Mail 3. Keep file size under 102KB - Gmail clips larger emails 4. Use keywords strategically - Increase engagement in email body 5. Inline styles as fallback - Some clients strip <style> tags
Related skills
How it compares
Use react-email for React-based email templates; use raw HTML email skills when you cannot adopt the React Email component library.
FAQ
Which components does react-email document?
react-email documents React Email library components including Body, Button, CodeBlock, CodeInline, and Column, imported from react-email and styled with the Tailwind helper for email-safe HTML output.
Why use react-email instead of regular React for emails?
react-email targets email-client constraints—table layouts, inline-safe styling, and tested components—where standard React DOM patterns break in Gmail, Outlook, and other clients. The skill guides correct component usage and imports.
Is React Email safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.