Handlebars template support for React Email and JSX Email
A library that bridges the gap between React-based email templates and Handlebars templating. Write your email templates once using React components, and generate Handlebars-compatible templates for dynamic content rendering.
- Dual Runtime Support: Works seamlessly with both React Email and JSX Email
- Type-Safe: Full TypeScript support with Zod schema validation
- Preview Mode: Preview your emails with sample data during development
- Build Mode: Generate Handlebars templates for production use
- Conditional Rendering:
IfandUnlesscomponents for conditional content - List Rendering:
Eachcomponent for iterating over data arrays
npm install react-email-handlebars zod
# or
pnpm add react-email-handlebars zod
# or
yarn add react-email-handlebars zodNote:
zodis required when using theEachcomponent for schema validation.
This library requires React 19 or higher:
npm install react@^19.0.0 react-dom@^19.0.0Wrap your email template with RuntimeProvider to specify the runtime mode:
preview: Renders with preview data (for development)build: Generates Handlebars template syntax (for production)
Conditionally render content based on a Handlebars path.
import { Body, Html, Text } from "@react-email/components";
import { If, RuntimeProvider } from "react-email-handlebars";
export default function WelcomeEmail() {
return (
<RuntimeProvider value="build">
<Html>
<Body>
<If
conditionPath="user.isAdmin"
previewCondition={false}
then={<Text>You are an admin</Text>}
else={<Text>You are not an admin</Text>}
/>
</Body>
</Html>
</RuntimeProvider>
);
}Generated Handlebars Template:
import { Body, Html, Text } from "jsx-email";
import { If, RuntimeProvider } from "react-email-handlebars";
export const Template = () => (
<RuntimeProvider value="preview">
<Html>
<Body>
<If
conditionPath="user.isPremium"
previewCondition={true}
then={<Text>Premium Features Unlocked!</Text>}
else={<Text>Upgrade to Premium</Text>}
/>
</Body>
</Html>
</RuntimeProvider>
);Generated Handlebars Template:
Inverse of If component. Renders content when a condition is falsy.
import { Body, Html, Text } from "@react-email/components";
import { Unless, RuntimeProvider } from "react-email-handlebars";
export default function PaymentReminderEmail() {
return (
<RuntimeProvider value="build">
<Html>
<Body>
<Unless
conditionPath="user.hasPaid"
previewCondition={false}
then={
<Text>Please make a payment to continue using our services.</Text>
}
else={<Text>Thank you for your payment!</Text>}
/>
</Body>
</Html>
</RuntimeProvider>
);
}Generated Handlebars Template:
import { Body, Html, Text } from "jsx-email";
import { Unless, RuntimeProvider } from "react-email-handlebars";
export const Template = () => (
<RuntimeProvider value="preview">
<Html>
<Body>
<Unless
conditionPath="user.isVerified"
previewCondition={false}
then={<Text>Please verify your email address.</Text>}
/>
</Body>
</Html>
</RuntimeProvider>
);Generated Handlebars Template:
Iterate over arrays and render items with type safety.
import { Body, Html, Text, Section } from "@react-email/components";
import { Each, RuntimeProvider } from "react-email-handlebars";
import { z } from "zod";
const userSchema = z.object({
name: z.string(),
email: z.string(),
});
type User = z.infer<typeof userSchema>;
export default function UserListEmail() {
const previewData: User[] = [
{ name: "John Doe", email: "[email protected]" },
{ name: "Jane Smith", email: "[email protected]" },
];
return (
<RuntimeProvider value="build">
<Html>
<Body>
<Section>
<Text>User List:</Text>
<Each
previewData={previewData}
each="users"
schema={userSchema}
renderItem={(user) => (
<div key={user.email}>
<Text>Name: {user.name}</Text>
<Text>Email: {user.email}</Text>
</div>
)}
/>
</Section>
</Body>
</Html>
</RuntimeProvider>
);
}Generated Handlebars Template:
import { Body, Html, Text } from "jsx-email";
import { Each, RuntimeProvider } from "react-email-handlebars";
import { z } from "zod";
const productSchema = z.object({
name: z.string(),
price: z.number(),
});
export const Template = () => {
const previewData = [
{ name: "Product A", price: 29.99 },
{ name: "Product B", price: 49.99 },
];
return (
<RuntimeProvider value="preview">
<Html>
<Body>
<Each
previewData={previewData}
each="products"
schema={productSchema}
renderItem={(product) => (
<Text key={product.name}>
{product.name}: ${product.price}
</Text>
)}
/>
</Body>
</Html>
</RuntimeProvider>
);
};Generated Handlebars Template:
The Each component supports nested objects in Zod schemas. You can access deep properties using standard dot notation or object destructuring.
const UserSchema = z.object({
id: z.string(),
info: z.object({
name: z.string(),
contact: z.object({
email: z.string(),
}),
}),
});
// ...
<Each
each="users"
schema={UserSchema}
previewData={previewUsers}
renderItem={(user) => (
<Text key={user.id}>
{user.info.name} - {user.info.contact.email}
</Text>
)}
/>;Generated Handlebars Template:
Props:
value:"preview" | "build"- Runtime mode
Props:
conditionPath:string- Handlebars path for the conditionpreviewCondition:boolean- Condition value for preview modethen:ReactNode- Content to render when condition is trueelse?:ReactNode- Optional content to render when condition is false
Props:
conditionPath:string- Handlebars path for the conditionpreviewCondition:boolean- Condition value for preview modethen:ReactNode- Content to render when condition is falseelse?:ReactNode- Optional content to render when condition is true
Props:
previewData:TItem[]- Array of items for preview modeeach:string- Handlebars path for the arrayschema:z.ZodSchema<TItem>- Zod schema defining item structurerenderItem:(item: TItem) => ReactNode- Function to render each item
pnpm installpnpm run buildpnpm run devcd examples/react-email
pnpm install
pnpm run devVisit http://localhost:3000 to see the examples.
cd examples/jsx-email
pnpm install
pnpm run devThis library uses @rstest/core for testing. Tests cover both preview and build modes for all components.
pnpm testtests/If.test.tsx: Verifies conditional rendering logic and Handlebars syntax generation.tests/Unless.test.tsx: Verifies inverse conditional logic and Handlebars syntax generation.tests/Each.test.tsx: Verifies list iteration, empty states, and schema validation.
The library provides two runtime modes:
-
Preview Mode (
runtime="preview"):- Uses the provided
previewDataandpreviewCondition - Renders actual React components
- Perfect for development and testing
- Uses the provided
-
Build Mode (
runtime="build"):- Generates Handlebars template syntax
- Uses Zod schemas to create placeholder variables
- Outputs template strings ready for production use
The runtime context is managed via React Context API, ensuring compatibility with both React Email and JSX Email environments.
MIT
Contributions are welcome! Please feel free to submit a Pull Request.