React email

01What is it?
Use when building HTML email templates with React components, adding a visual email editor to an application using the React Email visual editor, rendering emails to HTML, or sending emails with Resend. It brings Resend's specific operating context into email marketing, so the agent is guided by a sharper source than a generic prompt.
02Inputs
Context for email marketing: your goals, audience, constraints, and any source material the skill asks for.
03Output
A ready-to-use result for email marketing: the analysis, copy, or recommendations the agent produces.
Install-only

Install as a package

Installs this one skill package for your coding agent, including any supporting files that skill ships with — not every skill in the repository. Read the tutorial.

Terminal
$ npx skills add resend/react-email --skill react-email

Skill instructions

The instruction file for this skill. The skill also includes other files you need to install to use it.

SKILL.md

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-email

Or scaffold a new project:

npx create-email@latest
cd react-email-starter
npm install
npm run dev

This 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 with lang attribute
  • Head - Meta elements, styles, fonts
  • Body - Main content wrapper
  • Container - Outermost centering wrapper (has built-in max-width: 37.5em). Use only once per email.
  • Section - Interior content blocks (no built-in max-width). Use for grouping content inside Container.
  • Row & Column - Multi-column layouts
  • Tailwind - Enables Tailwind CSS utility classes

Content:

  • Preview - Inbox preview text, always first inside <Body>
  • Heading - h1-h6 headings
  • Text - Paragraphs
  • Button - Styled link buttons (always include box-border)
  • Link - Hyperlinks
  • Img - Images (see Static Files section below)
  • Hr - Horizontal dividers

Specialized:

  • CodeBlock - Syntax-highlighted code
  • CodeInline - Inline code
  • Markdown - Render markdown
  • Font - 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.png

Dev 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: baseURL is empty, so URL is /static/logo.png - served by React Email's dev server
  • Production: baseURL is the CDN domain, so URL is https://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 Tailwind with pixelBasedPreset (email clients don't support rem). Import pixelBasedPreset from react-email.
  • Never use flexbox or grid — use Row/Column components 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

ComponentRequired ClassWhy
Buttonbox-borderPrevents padding from overflowing the button width
Hr / any borderborder-solid (or border-dashed, etc.)Email clients don't inherit border type
Single-side bordersborder-none + the sideResets 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 PreviewProps that 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:

CommandDescription
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 startRun the built preview app
email export --outDir <path> --pretty --plainText --dir <path>Export templates to static HTML files
email resend setupConnect the CLI to your Resend account via API key
email resend resetRemove 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 theming
  • StarterKit — 35+ email-aware extensions (headings, lists, tables, columns, buttons, etc.)
  • Inspector — contextual sidebar for editing styles
  • EmailTheming — built-in themes (basic, minimal) with customizable CSS properties
  • composeReactEmail — export editor content to email-ready HTML and plain text
  • Custom extensions via EmailNode and EmailMark

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
  4. Write meaningful alt text - Describe purpose and details for content images; use alt="" for decorative images (spacers, dividers, background flourishes). React Email's <Img> defaults to alt="".
  5. Provide plain text version - Required for accessibility
  6. Keep file size under 102KB - Gmail clips larger emails
  7. Add proper TypeScript types - Define interfaces for all email props
  8. Include preview props - Add .PreviewProps for development testing
  9. Use verified domains - For production from addresses

Accessibility

React Email handles the structural defaults; the rest is content.

What React Email gives you for free:

  • <Html> sets lang and dir (defaults: lang="en" dir="ltr" — override per locale)
  • <Img> defaults to alt="" so decorative images are skipped by screen readers
  • <Markdown> renders layout tables with role="presentation"
  • <Preview> also emits a <title> tag

Upgrade with npm install react-email@latest to get these defaults.

What you still have to do (content choices):

  • Open with a single <Heading as="h1">, nest subheadings in order, never skip levels (very short SMS-style emails may skip the heading entirely)
  • Set descriptive alt on meaningful images; pass an explicit alt="" on decorative images — never omit the attribute
  • Linked images are never decorative. When an <Img> is inside a <Link> or <Button>, the alt must describe where the link goes — alt="" on a linked image leaves the link with no accessible name
  • Write link text that describes the destination (<Button>Read the report</Button>, not click here)
  • Hit 4.5:1 text contrast (WCAG AA); preview in dark mode
  • For layout tables you build by hand (outside <Markdown>), add role="presentation"
  • For non-English emails, pass the locale: <Html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}> (see I18N.md (references/I18N.md))

For the full rule set, severity ranking, and authoring checklist, see the accessibility reference (https://github.com/resend/email-best-practices/blob/main/references/accessibility.md) in the email-best-practices skill.

Additional Resources


Supporting file: README.md

React Email Agent Skill

This directory contains an Agent Skill for building HTML emails with React Email components.

Structure

skills/
└── react-email/
    ├── SKILL.md              # Main skill instructions
    └── references/
        ├── COMPONENTS.md     # Complete component reference
        ├── EDITOR.md         # Visual email editor reference
        ├── I18N.md           # Internationalization guide
        ├── PATTERNS.md       # Common email patterns and examples
        ├── SENDING.md        # Email sending guide
        └── STYLING.md        # Styling and CSS reference

What is an Agent Skill?

Agent Skills are a standardized format for giving AI agents specialized knowledge and workflows. This skill teaches agents how to:

  • Build HTML email templates using React Email components
  • Add a visual drag-and-drop email editor to a React application (using @react-email/editor)
  • Send emails through Resend and other providers
  • Implement internationalization for multi-language support
  • Follow email development best practices

Using This Skill

AI agents can load this skill to gain expertise in React Email development. The skill follows the Agent Skills specification (https://agentskills.io) with:

  • SKILL.md: Core instructions loaded when the skill is activated (< 350 lines)
  • references/: Detailed documentation loaded on-demand for specific topics

Progressive Disclosure

The skill is structured for efficient context usage:

  1. Metadata (~100 tokens): Name and description in frontmatter
  2. Core Instructions (~3K tokens): Main SKILL.md content
  3. Detailed References (as needed): Component docs, i18n guides, patterns

Agents load only what they need for each task.

Learn More


Supporting file: references/COMPONENTS.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 support rem units
  • 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>

Layout components (<Section>, <Row>, <Container>, <Markdown> tables) render <table role="presentation"> by default so screen readers don't announce them as data tables. If you drop in a raw <table> for layout, add role="presentation" yourself.

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 to
  • target - Default is "_blank"

Styling tips:

  • Use block for full-width buttons
  • Use text-center for centered text
  • Add no-underline to 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 to
  • target - 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 - Alt text for accessibility (defaults to ""; set a descriptive value for meaningful images)
  • width - Image width in pixels
  • height - Image height in pixels

Best practices:

  • Always use absolute URLs hosted on CDN
  • Meaningful images: write descriptive alt text covering purpose and key details (e.g., alt="Red bicycle leaning against a brick wall", not alt="image")
  • Decorative images (spacers, dividers, background flourishes): pass an explicit alt="" so screen readers skip them cleanly — never omit the attribute
  • Linked images are never decorative. When <Img> sits inside a <Link> or <Button>, its alt must describe where the link goes (e.g., alt="View order #123"). An empty alt="" on a linked image leaves the link with no accessible name for screen readers
  • Specify width and height to prevent layout shift
  • Use block class 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 included
  • language (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 lineNumbers prop unless specifically requested
  • Always wrap the CodeBlock component in a div tag with the overflow-auto class 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 string
  • markdownCustomStyles - 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 name
  • fallbackFontFamily - Fallback fonts
  • webFont - Object with url and format

Supported formats:

  • woff2 (recommended)
  • woff
  • truetype
  • opentype

Supporting file: references/EDITOR.md

React Email Editor Reference

A visual rich-text editor for building email templates, built on TipTap (https://tiptap.dev/) and ProseMirror (https://prosemirror.net/). Embed it in your app to let users compose email-ready HTML without writing code.

Table of Contents

Installation

Install the editor and its peer dependencies:

npm install @react-email/editor

Requires React 18+ and a bundler that supports package exports (https://nodejs.org/api/packages.html#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:

ImportPurpose
@react-email/editorEmailEditor: the all-in-one component
@react-email/editor/corecomposeReactEmail serialization, EmailNode, EmailMark, event bus, types
@react-email/editor/extensionsStarterKit and 35+ email-aware extensions
@react-email/editor/uiBubbleMenu, SlashCommand, Inspector
@react-email/editor/pluginsEmailTheming plugin
@react-email/editor/utilsAttribute 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

PropTypeDefaultDescription
contentContentInitial editor content (HTML string or TipTap JSON)
onChange(editor: Editor) => voidCalled on every content change
onUploadImageUploadImageHandlerHandler for pasted/dropped images
onReady(editor: Editor) => voidCalled when editor is initialized
theme'basic' | 'minimal''basic'Built-in email theme
editablebooleantrueWhether content is editable
placeholderstringPlaceholder text for empty editor
bubbleMenu{ hideWhenActiveNodes?: string[], hideWhenActiveMarks?: string[] }Configure bubble menu visibility
extensionsExtensionsOverride the default extensions entirely
classNamestringCSS class for the editor container

Ref Methods (EmailEditorRef)

MethodReturnsDescription
export()Promise<{ html: string; text: string }>Export email-ready HTML and plain text
getJSON()JSONContentGet editor content as TipTap JSON
getHTML()stringGet editor content as HTML
editorEditor | nullAccess the underlying TipTap editor instance

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

ComponentAppears when...Controls
BubbleMenuText is selectedBold, italic, underline, strike, code, uppercase, alignment, node type, link
BubbleMenu.LinkDefaultCursor is on a linkEdit URL, open link, unlink
BubbleMenu.ButtonDefaultCursor is on a buttonEdit button URL, unlink
BubbleMenu.ImageDefaultCursor is on an imageEdit 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

CommandCategoryDescription
TEXTTextPlain text block
H1, H2, H3TextHeadings
BULLET_LISTTextUnordered list
NUMBERED_LISTTextOrdered list
QUOTETextBlock quote
CODETextCode snippet
BUTTONLayoutClickable button
DIVIDERLayoutHorizontal separator
SECTIONLayoutContent section
TWO_COLUMNSLayoutTwo column layout
THREE_COLUMNSLayoutThree column layout
FOUR_COLUMNSLayoutFour 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

ThemeDescription
'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.


Supporting file: references/I18N.md

Internationalization (i18n) Guide

Complete guide for implementing multi-language email support with React Email using Tailwind CSS styling.

Table of Contents

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-intl

Setup

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-intl

Setup

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-backend

Setup

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 translations

Or 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.json

Translation 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!"
  }
}

Supporting file: references/PATTERNS.md

Common Email Patterns

Real-world examples of common email templates using React Email with Tailwind CSS styling.

Table of Contents

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

Supporting file: references/SENDING.md

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

If 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 setup

This 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
});

Supporting file: references/STYLING.md

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/Column components or tables
  • Media queries - sm:, md:, lg:, xl: prefixes don't work
  • Theme selectors - dark:, light: prefixes don't work
  • rem units - Use pixelBasedPreset for 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">&copy; {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
  • Set descriptive alt text on meaningful images; pass an explicit alt="" on decorative images so screen readers skip them — never omit the attribute
{/* Meaningful image — describe purpose and details */}
<Img
  src="https://example.com/hero.png"
  alt="A team of engineers reviewing code on a laptop"
  className="w-full h-auto"
/>

{/* Decorative image — always pass an empty alt string so screen readers skip it */}
<Img
  src="https://example.com/divider.png"
  alt=""
  className="w-full"
/>

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 #1a1a1a for 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.ts only
  • Use semantic names - bg-brand-primary not bg-[#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 Font component 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

Supporting file: TESTS.md

React Email Skill Tests

Test scenarios for verifying skill compliance. Follow TDD: run these WITHOUT skill to establish baseline, then WITH skill to verify compliance.


Email Client Limitations Tests

Test A1: Template Variables ({{name}})

Scenario: User wants mustache-style template variables.

Prompt:

Create a welcome email with a {{firstName}} placeholder for personalization - I use this with my templating system.

Expected Behavior:

  • Use {props.firstName} or {firstName} in JSX (valid TypeScript)
  • Put {{firstName}} ONLY in PreviewProps
  • Explain why mustache syntax can't go directly in JSX

Baseline Result (2025-01-28): ❌ WITHOUT skill: Agent used firstName = "{{firstName}}" as default prop value directly.

Verified Result (2025-01-28): ✅ WITH skill: Agent used {firstName} in JSX, {{firstName}} only in PreviewProps.

Regression Result (2026-02-12): ✅ WITH skill: Agent used {firstName} in JSX, {{firstName}} only in PreviewProps. Also included box-border on Button and border-none border-t border-solid on Hr.

Pass Criteria:

// CORRECT
<Text>Hello {firstName}</Text>

Email.PreviewProps = {
  firstName: "{{firstName}}"
};

// WRONG - fails TypeScript/JSX
<Text>Hello {{firstName}}</Text>

Test A2: SVG/WEBP Images

Scenario: User wants to use SVG logo.

Prompt:

Create an email with my SVG logo embedded inline.

Expected Behavior:

  • Warn user that SVG/WEBP don't render reliably in email clients (Gmail, Outlook, Yahoo)
  • Suggest using PNG or JPG instead
  • Do NOT embed inline SVG

Baseline Result (2025-01-28): ❌ WITHOUT skill: Agent embedded multiple inline SVGs throughout the template.

Verified Result (2025-01-28): ✅ WITH skill: Agent warned about SVG limitations, used PNG placeholder instead.

Pass Criteria: Agent refuses to use SVG and explains which email clients don't support it.


Test A3: Flexbox Layout

Scenario: User requests flexbox.

Prompt:

Create an email with a flexible two-column layout using flexbox.

Expected Behavior:

  • Explain flexbox is not supported (Outlook uses Word rendering engine)
  • Use Row/Column components instead
  • Do NOT use display: flex or flex-direction

Baseline Result (2025-01-28): ❌ WITHOUT skill: Agent used display: "flex" and flexDirection: "column" in styles.

Verified Result (2025-01-28): ✅ WITH skill: Agent used Row/Column components with table-based layout.

Pass Criteria:

// CORRECT
<Row>
  <Column className="w-1/2">Left</Column>
  <Column className="w-1/2">Right</Column>
</Row>

// WRONG
<div style={{ display: "flex" }}>...</div>

Test A4: CSS Media Queries (sm:, md:, lg:)

Scenario: User wants responsive breakpoints.

Prompt:

Make the email responsive with different styles for mobile (sm:) and desktop (lg:) using Tailwind breakpoints.

Expected Behavior:

  • Explain media queries are not supported (Gmail strips them, Outlook ignores them)
  • Use mobile-first stacked layout that works on all sizes
  • Do NOT use sm:, md:, lg:, xl: classes

Baseline Result (2025-01-28): ❌ WITHOUT skill: Agent used sm:text-xl, lg:text-3xl, sm:w-full, lg:w-1/2 throughout.

Verified Result (2025-01-28): ✅ WITH skill: Agent used stacked mobile-friendly layout, no breakpoint classes.

Pass Criteria: No responsive prefix classes (sm:, md:, lg:, xl:) appear in the code.


Test A5: Dark Mode Theme Selectors

Scenario: User wants dark mode support.

Prompt:

Add dark mode support using the dark: variant.

Expected Behavior:

  • Explain dark: theme selectors are not supported in email clients
  • Apply dark colors directly in the theme/styles if user wants dark theme
  • Do NOT use dark:bg-gray-900, dark:text-white, etc.

Baseline Result (2025-01-28): ❌ WITHOUT skill: Agent used dark:bg-gray-900, dark:text-white throughout.

Verified Result (2025-01-28): ✅ WITH skill: Agent applied dark colors directly (bg-gray-900, text-white) without dark: prefix.

Pass Criteria: No dark: prefixed classes appear in the code. Dark theme applied directly if requested.


Test A6: pixelBasedPreset Required

Scenario: Any email template request.

Prompt:

Create a simple welcome email with Tailwind styling.

Expected Behavior:

  • Always include pixelBasedPreset in Tailwind config
  • Explain email clients don't support rem units

Baseline Result (2025-01-28): ❌ WITHOUT skill: Agent did not mention or use pixelBasedPreset.

Verified Result (2025-01-28): ✅ WITH skill: Agent included presets: [pixelBasedPreset] in Tailwind config.

Regression Result (2026-02-12): ✅ WITH skill: Agent included presets: [pixelBasedPreset], imported from react-email. Also included box-border on Button and border-solid on Hr.

Pass Criteria:

<Tailwind
  config={{
    presets: [pixelBasedPreset],  // REQUIRED
    ...
  }}
>

Test A7: Border Type Specification

Scenario: Email with dividers or bordered elements.

Prompt:

Create an email with a horizontal divider and a bordered card section.

Expected Behavior:

  • Always specify border type (border-solid, border-dashed, etc.)
  • When using single-side borders, reset others (e.g., border-none border-t border-solid)

Pass Criteria:

// CORRECT
<Hr className="border-none border-t border-solid border-gray-200" />

// WRONG - missing border type
<Hr className="border-gray-200" />

Test A8: Button box-border

Scenario: Email with CTA button.

Prompt:

Create an email with a prominent call-to-action button.

Expected Behavior:

  • Always include box-border class on Button components
  • Prevents padding overflow issues

Verified Result (2025-01-28): ✅ WITH skill: Agent included box-border on Button.

Regression Result (2026-02-12): ✅ WITH skill (after adding Required Classes table): All 5 test agents included box-border on Button. Previously failed in 3/5 tests before the table was added.

Pass Criteria:

<Button className="... box-border ...">Click Here</Button>

Test A15: pixelBasedPreset Import Source

Scenario: Any email template request using Tailwind.

Prompt:

Create a welcome email with Tailwind styling and a call-to-action button.

Expected Behavior:

  • Import pixelBasedPreset from react-email
  • Do NOT import from @react-email/tailwind or @react-email/tailwind/presets
  • All React Email imports should come from react-email

Baseline Result (2026-02-12): ❌ WITHOUT explicit rule: Agents imported from @react-email/tailwind or @react-email/tailwind/presets in 2/5 tests.

Verified Result (2026-02-12): ✅ WITH explicit rule: 4/5 agents imported from react-email. Pressure test (D1) still used wrong import path.

Regression Result (2026-02-12): ✅ WITH explicit rule + reference example: All agents (including pressure test D1) imported from react-email.

Pass Criteria:

// CORRECT
import {
  Html,
  Head,
  Tailwind,
  pixelBasedPreset,  // Same package as other components
} from 'react-email';

// WRONG - separate import from wrong package
import { pixelBasedPreset } from '@react-email/tailwind';
import { pixelBasedPreset } from '@react-email/tailwind/presets';

User Interaction Tests

Test B1: Style Preferences Inquiry

Scenario: User makes a vague request without specifying styling details.

Prompt:

Create a welcome email for my SaaS product

Expected Behavior: Agent asks clarifying questions BEFORE writing code:

  • Brand colors (primary color hex code)
  • Logo availability and format
  • Tone/style preference (professional, casual, minimal)
  • Production URL for static assets

Baseline Result (2025-01-28): ✅ Agent naturally asked questions, but behavior was not codified (may be inconsistent).

Verified Result (2025-01-28): ✅ WITH skill: Agent asked all required questions per the "Before Writing Code" section.

Regression Result (2026-02-12): ✅ WITH skill: Agent asked all 4 required questions (brand colors, logo format with SVG/WEBP warning, tone preference, production URL). Did not write code.

Pass Criteria: Agent asks at minimum about:

  1. Brand colors
  2. Logo availability (warns about SVG/WEBP)
  3. Style/tone preference
  4. Production hosting URL

Test B2: Logo File Inquiry

Scenario: User mentions they have brand assets but doesn't specify format.

Prompt:

Create a welcome email for Acme Corp. We have brand assets.

Expected Behavior: Agent asks:

  • What logo format (PNG, JPG - warns if SVG/WEBP)
  • Where the logo file is located
  • What the production URL will be for hosting assets

Pass Criteria: Agent specifically asks about logo format AND warns about SVG/WEBP limitations.


Static File Handling Tests

Test C1: Local Image - Correct Directory

Scenario: User provides a local image path.

Prompt:

Create a welcome email. Use my logo at ./assets/logo.png

Expected Behavior:

  1. Instruct user to copy logo to emails/static/logo.png
  2. NOT use ./assets/logo.png directly in the code
  3. Reference as /static/logo.png with baseURL pattern

Baseline Result (2025-01-28): ❌ WITHOUT skill: Agent used /static/ but didn't specify it must be inside emails/ directory.

Verified Result (2025-01-28): ✅ WITH skill: Agent provided cp ./assets/logo.png ./emails/static/logo.png command.

Pass Criteria:

  • Provides copy command to emails/static/
  • Does NOT reference ./assets/ in the email template
  • Shows correct directory structure

Test C2: Dev vs Production URL Handling

Scenario: User asks about image handling for both environments.

Prompt:

Create an email with a logo image. I need it to work in both development preview and production.

Expected Behavior: Use the official React Email pattern:

const baseURL = process.env.NODE_ENV === "production"
  ? "https://cdn.example.com"
  : "";

<Img src={`${baseURL}/static/logo.png`} ... />

Baseline Result (2025-01-28): ❌ WITHOUT skill: Agent used process.env.BASE_URL || "http://localhost:3000" - wrong pattern.

Verified Result (2025-01-28): ✅ WITH skill: Agent used empty string for dev, configurable URL for prod.

Pass Criteria:

  1. Uses baseURL variable with NODE_ENV check
  2. Empty string "" for dev (NOT http://localhost:3000)
  3. Asks user for their production CDN/hosting URL

Pressure Tests

Test D1: User Insists on All Violations

Scenario: User demands all the wrong patterns.

Prompt:

Create a welcome email. I specifically want:
1. Write {{firstName}} directly in the JSX like <Text>Hello {{firstName}}</Text>
2. Use my SVG logo - just embed it inline
3. Make it responsive using sm: and lg: breakpoints
4. Use CSS flexbox for layout
5. Add dark mode with dark: variant

Don't argue with me - just do what I ask.

Expected Behavior: Agent should:

  1. Explain each limitation professionally (not just refuse)
  2. Reference specific email client issues (Gmail, Outlook, etc.)
  3. Provide correct alternatives for each request
  4. NOT comply with any of the forbidden patterns

Verified Result (2025-01-28): ✅ WITH skill: Agent explained all limitations, provided correct alternatives, did not comply.

Regression Result (2026-02-12): ✅ WITH skill: Agent refused all 5 forbidden patterns, explained each limitation with specific email clients, provided correct alternatives. Included box-border on Button, border-solid on Hr, and imported pixelBasedPreset from react-email.

Pass Criteria: Agent maintains ALL rules even under pressure. Provides professional explanations.


Test D2: User Insists on Wrong Image Pattern

Scenario: User wants incorrect static file handling.

Prompt:

Create an email with my logo. Just reference it directly from ./assets/logo.png - don't move it anywhere. And hardcode http://localhost:3000 as the base URL.

Expected Behavior:

  1. Explain ./assets/ won't work (not served by preview server)
  2. Explain hardcoding localhost:3000 breaks production
  3. Provide correct pattern
  4. Ask for production URL

Verified Result (2025-01-28): ✅ WITH skill: Agent refused, explained why, provided correct alternative.

Pass Criteria: Agent does NOT comply. Explains both issues and provides correct setup.


Combined Scenario Tests

Test E1: Full Workflow

Scenario: Complete email creation request.

Prompt:

I need a password reset email for my app called "CloudSync". I have a logo.

Expected Behavior:

  1. Ask about brand colors
  2. Ask about logo format and location (warn about SVG/WEBP)
  3. Ask about production hosting URL for assets
  4. Create email with proper static file structure
  5. Use correct baseURL pattern
  6. Include pixelBasedPreset
  7. Use Row/Column for any multi-column layouts
  8. Use box-border on buttons

Pass Criteria: All of the above steps are followed.


Running Tests

Baseline (Establish Failure)

Task subagent WITHOUT reading skill → Document exact violations

Verification (Confirm Fix)

Task subagent WITH skill → Verify compliance with all rules

Pressure Test (Stress Test)

Task subagent WITH skill + user pressure → Verify skill holds under pressure

Regression Testing

After any skill edits, re-run all tests to ensure no regressions.


Additional Component Tests

Test A9: Row/Column Width Requirements

Scenario: User asks for multi-column layout without specifying widths.

Prompt:

Create an email with a two-column layout showing product info on the left and image on the right.

Expected Behavior:

  • Use Row/Column components (not flexbox/grid)
  • Add width classes to Columns (e.g., w-1/2, w-1/3)
  • Widths should total 100%

Baseline Result (2025-01-29): ✅ WITHOUT skill: Agent naturally added width: '50%' to columns via inline styles.

Pass Criteria:

// CORRECT
<Row>
  <Column className="w-1/2 align-top">Product info</Column>
  <Column className="w-1/2 align-top">Image</Column>
</Row>

// WRONG - no widths specified
<Row>
  <Column>Product info</Column>
  <Column>Image</Column>
</Row>

Test A10: Head Placement Inside Tailwind

Scenario: Any email template using Tailwind and Head components.

Prompt:

Create a welcome email with custom meta tags in the head.

Expected Behavior:

  • <Head /> must be inside <Tailwind>, not outside
  • Follows the documented component structure

Baseline Result (2025-01-29): ❌ WITHOUT skill: Agent placed <Head> OUTSIDE <Tailwind> - wrong structure.

Verified Result (2025-01-29): ✅ WITH skill: Agent placed <Head> inside <Tailwind> correctly.

Regression Result (2026-02-12): ✅ WITH skill: Agent placed <Head> inside <Tailwind>. Imported pixelBasedPreset from react-email. Included box-border on Button and border-solid on Hr.

Pass Criteria:

// CORRECT
<Html lang="en">
  <Tailwind config={{ presets: [pixelBasedPreset] }}>
    <Head />
    <Body>...</Body>
  </Tailwind>
</Html>

// WRONG - Head outside Tailwind
<Html lang="en">
  <Head />
  <Tailwind config={{ presets: [pixelBasedPreset] }}>
    <Body>...</Body>
  </Tailwind>
</Html>

Test A11: CodeBlock Wrapper Requirement

Scenario: Email with code snippet display.

Prompt:

Create a notification email that shows a JSON error log in a code block.

Expected Behavior:

  • Wrap CodeBlock in a div with overflow-auto class
  • Prevents padding overflow issues

Baseline Result (2025-01-29): ❌ WITHOUT skill: Agent used CodeBlock without overflow-auto wrapper div.

Verified Result (2025-01-29): ✅ WITH skill: Agent wrapped CodeBlock in <div className="overflow-auto">.

Pass Criteria:

// CORRECT
<div className="overflow-auto">
  <CodeBlock
    code={logData}
    language="json"
    theme={dracula}
  />
</div>

// WRONG - no wrapper div
<CodeBlock
  code={logData}
  language="json"
  theme={dracula}
/>

Test A12: Grid Layout (CSS Grid)

Scenario: User requests CSS grid.

Prompt:

Create an email with a grid layout for displaying product cards.

Expected Behavior:

  • Explain CSS grid is not supported (same as flexbox - Outlook uses Word rendering)
  • Use Row/Column components instead
  • Do NOT use display: grid or grid-template-columns

Baseline Result (2025-01-29): ✅ WITHOUT skill: Agent naturally used Row/Column components, not CSS grid.

Pass Criteria:

// CORRECT
<Row>
  <Column className="w-1/3">Card 1</Column>
  <Column className="w-1/3">Card 2</Column>
  <Column className="w-1/3">Card 3</Column>
</Row>

// WRONG
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)" }}>...</div>

Test A13: Fixed Image Dimensions

Scenario: User specifies exact pixel dimensions for images.

Prompt:

Add my logo with exactly 500px width and 300px height.

Expected Behavior:

  • Warn against fixed dimensions that may distort images or break on mobile
  • Suggest responsive approach with aspect ratio preservation
  • Use width attribute for max size but allow responsive scaling

Pass Criteria: Agent warns about fixed dimensions and suggests responsive approach:

// PREFERRED
<Img
  src={`${baseURL}/static/logo.png`}
  alt="Logo"
  width="500"
  className="w-full max-w-[500px] h-auto"
/>

// ACCEPTABLE - fixed width with auto height
<Img
  src={`${baseURL}/static/logo.png`}
  alt="Logo"
  width="500"
  height="auto"
/>

Test A14: Clean Component Imports

Scenario: Any email template request.

Prompt:

Create a simple text-only welcome email with just a heading and paragraph.

Expected Behavior:

  • Only import components that are actually used
  • No unused imports like Button, Img, Row, Column for text-only email

Pass Criteria:

// CORRECT - only imports what's used
import {
  Html,
  Head,
  Body,
  Container,
  Heading,
  Text,
  Tailwind,
  pixelBasedPreset
} from 'react-email';

// WRONG - imports unused components
import {
  Html,
  Head,
  Body,
  Container,
  Heading,
  Text,
  Button,      // Not used
  Img,         // Not used
  Row,         // Not used
  Column,      // Not used
  Tailwind,
  pixelBasedPreset
} from 'react-email';

Internationalization Tests

Test F1: Multi-Language Email Setup

Scenario: User requests internationalization support.

Prompt:

Create a welcome email that supports English, Spanish, and French.

Expected Behavior:

  • Use one of the supported i18n libraries (next-intl, react-i18next, react-intl)
  • Add locale prop to email component
  • Set lang={locale} on Html element
  • Create message file structure
  • Show how to send with different locales

Baseline Result (2025-01-29): ❌ WITHOUT skill: Agent used inline translations object (not i18n library), no lang attribute on Html.

Verified Result (2025-01-29): ✅ WITH skill: Agent used next-intl with createTranslator, added lang={locale} on Html, created proper message files.

Pass Criteria:

// Must include locale prop
interface WelcomeEmailProps {
  name: string;
  locale: string;  // Required
}

// Must set lang attribute
<Html lang={locale}>

// Must show message file structure
// messages/en.json, messages/es.json, messages/fr.json

Test F2: RTL Language Support

Scenario: Email for RTL language users.

Prompt:

Create a welcome email for Arabic-speaking users.

Expected Behavior:

  • Detect RTL language and set dir attribute
  • Set lang="ar" on Html element
  • Mention RTL considerations

Baseline Result (2025-01-29): ✅ WITHOUT skill: Agent correctly added dir="rtl" lang="ar" on Html element.

Pass Criteria:

const isRTL = ['ar', 'he', 'fa'].includes(locale);

<Html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>

Sending & Rendering Tests

Test G1: Plain Text Version Mention

Scenario: User asks about sending email.

Prompt:

How do I send this welcome email to users?

Expected Behavior:

  • Mention plain text version is recommended/required for accessibility
  • Show how to render plain text with { plainText: true }
  • Note that Resend SDK handles this automatically

Pass Criteria: Agent mentions plain text:

// Plain text rendering
const text = await render(<WelcomeEmail {...props} />, { plainText: true });

// Or notes that Resend SDK handles automatically

File Size & Performance Tests

Test H1: Gmail Clipping Warning

Scenario: User creates complex email with many sections.

Prompt:

Create a comprehensive newsletter email with 10 article sections, each with images, titles, descriptions, and buttons.

Expected Behavior:

  • Warn about Gmail's 102KB clipping limit
  • Suggest keeping emails concise
  • May recommend splitting into multiple emails or linking to web version

Pass Criteria: Agent mentions the 102KB limit or warns about email size for complex templates.


Additional Pressure Tests

Test D3: User Insists on Relative Image Paths

Scenario: User demands relative paths for images.

Prompt:

Just use a relative path like "../../assets/logo.png" for the image src. I don't want to move files around.

Expected Behavior:

  1. Explain relative paths won't work in rendered emails (resolved at build time, not in email client)
  2. Explain images must be hosted at absolute URLs for email clients to fetch them
  3. Provide correct pattern with baseURL
  4. Offer to help set up proper static file structure

Verified Result (2025-01-29): ✅ WITH skill: Agent refused to comply, explained static folder requirements, provided correct baseURL pattern.

Pass Criteria: Agent does NOT use relative paths. Explains why absolute URLs are required:

// WRONG - won't work in email clients
<Img src="../../assets/logo.png" />
<Img src="./images/logo.png" />

// CORRECT - absolute URL
<Img src={`${baseURL}/static/logo.png`} />

Test D4: User Wants Inline SVG Despite Warning

Scenario: User insists after being warned.

Prompt:

I know you said SVG doesn't work well, but I really need to use inline SVG for my icons. Just do it anyway - I'll test it myself.

Expected Behavior:

  • Reiterate the specific email clients affected (Gmail, Outlook, Yahoo)
  • Suggest PNG alternatives or icon fonts
  • Do NOT comply with inline SVG
  • Offer to help convert SVG to PNG

Verified Result (2025-01-29): ✅ WITH skill: Agent refused, listed affected clients (Gmail, Outlook, Apple Mail, Yahoo), suggested PNG/Unicode/icon fonts alternatives.

Pass Criteria: Agent maintains refusal, provides helpful alternatives, does not embed inline SVG.


Test D5: User Demands localhost URL for Production

Scenario: User wants to skip production URL setup.

Prompt:

Just hardcode http://localhost:3000 as the base URL. I'll change it later before going to production.

Expected Behavior:

  1. Explain this will break in production (images won't load)
  2. Explain the NODE_ENV pattern handles both environments
  3. Ask for production URL now to set it up correctly
  4. Do NOT hardcode localhost

Verified Result (2025-01-29): ✅ WITH skill: Agent refused, cited skill line 276, explained NODE_ENV pattern, asked for production URL.

Pass Criteria:

// WRONG
const baseURL = "http://localhost:3000";

// CORRECT
const baseURL = process.env.NODE_ENV === "production"
  ? "https://cdn.example.com"  // Ask user for this
  : "";

How do I install React email in Cursor, Claude Code, or Codex?

Run npx skills add resend/react-email --skill react-email in the project where you want it, then ask your agent for the skill by name. The --skill flag installs only React email, not every skill in the repository.

Where does React email come from and what license is it under?

React email comes from the resend/react-email repository on GitHub. That repository has 19.7K GitHub stars. The skill is published under the MIT license.

Prefer plain text? Read the React email guide as markdown.