Complete TypeScript reference for every @react-email/components component. Props, types, gotchas, and patterns you'll actually use.
- Quick-reference table of all components and their key typed props
- Copy-paste code snippets for layout and content components
- Patterns for typing your own custom templates (unions, generics, arrays)
- Common TypeScript errors with React Email and how to fix them
Component props reference
Every component in @react-email/components extends its corresponding HTML element's attributes. The table below lists the key props you'll use most often — all components also accept style, className, and standard HTML attributes.
| Component | Key Props | TypeScript Type |
|---|---|---|
| Html | lang | React.ComponentPropsWithoutRef<"html"> |
| Head | (children only) | React.ComponentPropsWithoutRef<"head"> |
| Body | style | React.ComponentPropsWithoutRef<"body"> |
| Preview | children (text) | { children: string } |
| Container | style | React.ComponentPropsWithoutRef<"table"> |
| Section | style | React.ComponentPropsWithoutRef<"table"> |
| Row | style | React.ComponentPropsWithoutRef<"table"> |
| Column | style | React.ComponentPropsWithoutRef<"td"> |
| Text | style | React.ComponentPropsWithoutRef<"p"> |
| Heading | as, m, mx, my, mt, mb | HeadingProps (custom) |
| Link | href, target | React.ComponentPropsWithoutRef<"a"> |
| Button | href, style | ButtonProps (custom, renders <a>) |
| Img | src, alt, width, height | React.ComponentPropsWithoutRef<"img"> |
| Hr | style | React.ComponentPropsWithoutRef<"hr"> |
| Font | fontFamily, fallbackFontFamily, webFont, fontWeight, fontStyle | FontProps (custom) |
| Tailwind | config | { config?: TailwindConfig; children: React.ReactNode } |
All components also accept standard HTML attributes and style objects.
Container, Section, and Row use table-based prop types even though they feel like div-based layout primitives.Layout components
Layout components control the structural skeleton of your email. Here are the typed patterns for each one.
Html, Head, and Body
import {
Html,
Head,
Body,
Preview,
Font,
} from "@react-email/components";
interface BaseLayoutProps {
preview: string;
children: React.ReactNode;
}
export function BaseLayout({ preview, children }: BaseLayoutProps) {
return (
<Html lang="en" dir="ltr">
<Head>
<Font
fontFamily="Inter"
fallbackFontFamily="Helvetica"
webFont={{
url: "https://fonts.gstatic.com/s/inter/v13/UcCO3Fwrk3s.woff2",
format: "woff2",
}}
fontWeight={400}
fontStyle="normal"
/>
</Head>
<Preview>{preview}</Preview>
<Body style={{ backgroundColor: "#f9fafb", fontFamily: "Inter, sans-serif" }}>
{children}
</Body>
</Html>
);
}Container, Section, Row, and Column
import {
Container,
Section,
Row,
Column,
} from "@react-email/components";
// Container constrains width (renders as a centered <table>)
// Section groups rows (renders as a <table>)
// Row is a <table> row wrapper
// Column maps to a <td>
interface TwoColumnSectionProps {
left: React.ReactNode;
right: React.ReactNode;
}
export function TwoColumnSection({ left, right }: TwoColumnSectionProps) {
return (
<Container style={{ maxWidth: "600px", margin: "0 auto" }}>
<Section style={{ padding: "20px 0" }}>
<Row>
<Column style={{ width: "50%", verticalAlign: "top" }}>
{left}
</Column>
<Column style={{ width: "50%", verticalAlign: "top" }}>
{right}
</Column>
</Row>
</Section>
</Container>
);
}Column renders as a <td>, so width must be a string (“50%” or “300px”), not a bare number. The TypeScript type accepts both, but some email clients ignore numeric widths.Content components
Text and Heading
import { Text, Heading } from "@react-email/components";
// Text renders as <p> — accepts all <p> HTML attributes
<Text style={{ fontSize: "16px", lineHeight: "24px", color: "#374151" }}>
Your trial ends in 3 days.
</Text>
// Heading accepts an "as" prop to control the rendered tag
// Plus margin shorthand props: m, mx, my, mt, mb, ml, mr
<Heading
as="h1"
style={{ fontSize: "24px", fontWeight: 600, color: "#111827" }}
mt={0}
mb={16}
>
Welcome aboard
</Heading>
// Heading margin props are typed as numbers (pixels)
// as is typed as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
type HeadingAs = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";Link and Button
import { Link, Button } from "@react-email/components";
// Link renders as <a> — standard anchor props
<Link
href="https://app.example.com/settings"
target="_blank"
style={{ color: "#2563eb", textDecoration: "underline" }}
>
Manage your account
</Link>
// Button renders as a padded <a> with table-based styling
// Key difference: Button uses padding props, not a <button> element
<Button
href="https://app.example.com/verify?token=abc123"
style={{
backgroundColor: "#2563eb",
color: "#ffffff",
padding: "12px 24px",
borderRadius: "6px",
fontWeight: 600,
fontSize: "14px",
textDecoration: "none",
}}
>
Verify email address
</Button>Button is not a <button> element. It renders as an <a> tag with table-based padding for Outlook compatibility. The href prop is required — without it, you get a link that goes nowhere.Img and Hr
import { Img, Hr } from "@react-email/components";
// Img renders as <img> — width and height accept string or number
<Img
src="https://cdn.example.com/logo.png"
alt="Company logo"
width="120"
height="40"
style={{ display: "block", margin: "0 auto" }}
/>
// Hr renders as <hr> — simple horizontal rule
<Hr style={{
borderColor: "#e5e7eb",
borderTop: "1px solid #e5e7eb",
margin: "24px 0",
}} />Typing your own templates
The built-in component types handle the rendering layer. The harder problem is typing the data flowing into your templates. Here are the patterns that scale.
Basic props interface
// Start with a base type for props shared across all emails
interface BaseEmailProps {
previewText?: string;
}
// Extend it for each template
interface WelcomeEmailProps extends BaseEmailProps {
name: string;
loginUrl: string;
trialDays: number;
}
// Use it in the component
export default function WelcomeEmail({
name,
loginUrl,
trialDays,
previewText = `Welcome, ${name}! Your trial starts now.`,
}: WelcomeEmailProps) {
// ...
}Optional vs required props
interface NotificationEmailProps {
// Required — the email won't make sense without these
userName: string;
notificationType: string;
message: string;
// Optional — sensible defaults exist
actionUrl?: string;
actionLabel?: string;
unsubscribeUrl?: string;
previewText?: string;
}
export default function NotificationEmail({
userName,
notificationType,
message,
actionUrl,
actionLabel = "View details",
unsubscribeUrl,
previewText,
}: NotificationEmailProps) {
// actionUrl being optional means you must guard before rendering
// TypeScript enforces this — you can't access actionUrl.toString()
// without checking it first
return (
// ...
<>
{actionUrl && (
<Button href={actionUrl}>{actionLabel}</Button>
)}
</>
);
}Array props for line items
interface OrderItem {
name: string;
quantity: number;
unitPrice: number;
imageUrl?: string;
}
interface OrderConfirmationProps {
customerName: string;
orderNumber: string;
items: OrderItem[]; // Array of typed items
subtotal: number;
tax: number;
total: number;
currency: string;
shippingAddress: { // Nested object type
line1: string;
line2?: string;
city: string;
state: string;
zip: string;
country: string;
};
}
export default function OrderConfirmation({
items,
total,
currency,
// ...
}: OrderConfirmationProps) {
const fmt = new Intl.NumberFormat("en-US", {
style: "currency",
currency,
});
return (
<Section>
{items.map((item, i) => (
<Row key={i}>
<Column>{item.name}</Column>
<Column>{item.quantity}x</Column>
<Column>{fmt.format(item.unitPrice)}</Column>
</Row>
))}
<Row>
<Column>Total</Column>
<Column>{fmt.format(total)}</Column>
</Row>
</Section>
);
}Union types for email variants
// Use discriminated unions when one template handles multiple scenarios
type SubscriptionEmailProps =
| {
type: "trial_ending";
daysRemaining: number;
upgradeUrl: string;
}
| {
type: "payment_failed";
lastFour: string;
retryUrl: string;
nextRetryDate: string;
}
| {
type: "cancelled";
endDate: string;
reactivateUrl: string;
};
export default function SubscriptionEmail(props: SubscriptionEmailProps) {
// TypeScript narrows the type after checking the discriminant
switch (props.type) {
case "trial_ending":
// props.daysRemaining is available here
// props.lastFour would be a type error
return <TrialEndingContent daysRemaining={props.daysRemaining} />;
case "payment_failed":
return <PaymentFailedContent lastFour={props.lastFour} />;
case "cancelled":
return <CancelledContent endDate={props.endDate} />;
}
}Generic email wrapper component
import { Html, Head, Body, Preview, Container } from "@react-email/components";
// A generic wrapper that enforces structure without constraining content
interface EmailWrapperProps<TContent extends Record<string, unknown>> {
preview: string;
content: TContent;
renderContent: (content: TContent) => React.ReactNode;
}
export function EmailWrapper<TContent extends Record<string, unknown>>({
preview,
content,
renderContent,
}: EmailWrapperProps<TContent>) {
return (
<Html lang="en">
<Head />
<Preview>{preview}</Preview>
<Body style={{ backgroundColor: "#f9fafb" }}>
<Container style={{ maxWidth: "600px", margin: "0 auto", padding: "40px 20px" }}>
{renderContent(content)}
</Container>
</Body>
</Html>
);
}
// Usage — TypeScript infers TContent from the content prop
<EmailWrapper
preview="Your invoice is ready"
content={{ invoiceId: "INV-001", amount: 99.99 }}
renderContent={({ invoiceId, amount }) => (
<Text>Invoice {invoiceId}: ${amount}</Text>
)}
/>Common type errors and fixes
These are the TypeScript errors you'll run into most often with React Email. Each one has a quick fix.
| Error | Cause | Fix |
|---|---|---|
| Type 'number' is not assignable to type 'string' | Passing width={600} instead of width="600" on Img | Use string values for width/height: width="600" |
| Property 'href' is missing in type | Button component requires href | Add href prop — Button renders as <a>, not <button> |
| Type 'Element[]' is not assignable to type 'string' | Passing JSX children to Preview | Preview only accepts a plain string, not JSX elements |
| No overload matches this call (style prop) | Using CSS properties that don't exist in CSSProperties | Use camelCase: backgroundColor not background-color |
| 'Tailwind' cannot be used as a JSX component | @types/react version mismatch with React 19 | Update @types/react or add overrides in package.json |
React 19 and @types/react conflicts
If you're using React 19, you may hit type conflicts with older @types/react versions. React Email components may show errors like 'X' cannot be used as a JSX component.
{
"overrides": {
"@types/react": "^19.0.0"
}
}If you use pnpm, use pnpm.overrides instead:
{
"pnpm": {
"overrides": {
"@types/react": "^19.0.0"
}
}
}@types/react version mismatch.Style object typing
import type { CSSProperties } from "react";
// Define reusable style objects with the correct type
const baseTextStyle: CSSProperties = {
fontSize: "16px",
lineHeight: "24px",
color: "#374151",
fontFamily: "Inter, Helvetica, Arial, sans-serif",
};
// This catches typos and invalid values at compile time
const buttonStyle: CSSProperties = {
backgroundColor: "#2563eb",
color: "#ffffff",
padding: "12px 24px",
borderRadius: "6px",
// backgroundColour: "#2563eb", // Typo → TypeScript error
};
// You can also use satisfies for inline objects
const headerStyle = {
fontSize: "28px",
fontWeight: 700,
color: "#111827",
} satisfies CSSProperties;Validation with Zod
TypeScript types only exist at compile time. If your email data comes from an API, webhook, or database query at runtime, you need validation too. Zod lets you define a schema once and infer the TypeScript type from it.
import { z } from "zod";
const orderItemSchema = z.object({
name: z.string().min(1),
quantity: z.number().int().positive(),
unitPrice: z.number().nonnegative(),
});
export const orderConfirmationSchema = z.object({
customerName: z.string().min(1),
orderNumber: z.string().min(1),
items: z.array(orderItemSchema).min(1),
total: z.number().nonnegative(),
currency: z.string().length(3),
});
// Infer the TypeScript type — single source of truth
export type OrderConfirmationData = z.infer<typeof orderConfirmationSchema>;Already covered in depth
For a full walkthrough of Zod validation, type-safe send helpers, and preview data patterns, see Type-Safe Email Templates with TypeScript. This cheatsheet focuses on the component-level types; that post covers the end-to-end pipeline.
- Zero runtime cost — stripped during build
- Full IDE autocomplete and refactoring support
- Catches wrong prop types, missing fields, typos instantly
- Adds a runtime dependency (~50KB)
- Required when data crosses trust boundaries (APIs, webhooks)
- Provides better error messages for debugging bad data
In practice, use both. TypeScript for the template component signatures, Zod for validating external data before it reaches your templates.
React Email components are fully typed out of the box. The real work is typing the data layer — your template props, API inputs, and preview data.
- Use explicit
interfacetypes for every template's props — neverany - Extend a
BaseEmailPropstype for shared fields likepreviewText - Use discriminated unions when one template handles multiple email variants
- Type style objects with
React.CSSPropertiesto catch CSS typos - Add
@types/reactoverrides inpackage.jsonif you hit React 19 JSX component errors - Validate runtime data with Zod; infer types from schemas with
z.infer - Use
satisfieson preview props to keep them in sync with your type definitions