Code Tips11 min read

@react-email/components TypeScript Cheatsheet: Every Prop, Type, and Pattern

Complete TypeScript reference for @react-email/components. Props tables, typed layout and content examples, custom template patterns, and fixes for common type errors.

R

React Emails Pro

March 18, 2026

TL;DR

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.

ComponentKey PropsTypeScript Type
HtmllangReact.ComponentPropsWithoutRef<"html">
Head(children only)React.ComponentPropsWithoutRef<"head">
BodystyleReact.ComponentPropsWithoutRef<"body">
Previewchildren (text){ children: string }
ContainerstyleReact.ComponentPropsWithoutRef<"table">
SectionstyleReact.ComponentPropsWithoutRef<"table">
RowstyleReact.ComponentPropsWithoutRef<"table">
ColumnstyleReact.ComponentPropsWithoutRef<"td">
TextstyleReact.ComponentPropsWithoutRef<"p">
Headingas, m, mx, my, mt, mbHeadingProps (custom)
Linkhref, targetReact.ComponentPropsWithoutRef<"a">
Buttonhref, styleButtonProps (custom, renders <a>)
Imgsrc, alt, width, heightReact.ComponentPropsWithoutRef<"img">
HrstyleReact.ComponentPropsWithoutRef<"hr">
FontfontFamily, fallbackFontFamily, webFont, fontWeight, fontStyleFontProps (custom)
Tailwindconfig{ config?: TailwindConfig; children: React.ReactNode }

All components also accept standard HTML attributes and style objects.

React Email components render to HTML tables under the hood for email client compatibility. That's why 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

emails/base-layout.tsx
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

emails/two-column-layout.tsx
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

emails/content-example.tsx
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";
emails/actions.tsx
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

emails/media.tsx
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

emails/types.ts
// 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

emails/notification.tsx
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

emails/order-confirmation.tsx
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

emails/subscription-email.tsx
// 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} />;
  }
}
Discriminated unions are particularly useful for transactional emails where the same “category” of email has different data shapes. TypeScript narrows the type inside each branch automatically.

Generic email wrapper component

emails/email-wrapper.tsx
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.

ErrorCauseFix
Type 'number' is not assignable to type 'string'Passing width={600} instead of width="600" on ImgUse string values for width/height: width="600"
Property 'href' is missing in typeButton component requires hrefAdd href prop — Button renders as <a>, not <button>
Type 'Element[]' is not assignable to type 'string'Passing JSX children to PreviewPreview only accepts a plain string, not JSX elements
No overload matches this call (style prop)Using CSS properties that don't exist in CSSPropertiesUse camelCase: backgroundColor not background-color
'Tailwind' cannot be used as a JSX component@types/react version mismatch with React 19Update @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.

package.json
{
  "overrides": {
    "@types/react": "^19.0.0"
  }
}

If you use pnpm, use pnpm.overrides instead:

package.json
{
  "pnpm": {
    "overrides": {
      "@types/react": "^19.0.0"
    }
  }
}
This is the single most common source of frustration with React Email and TypeScript. If components render fine but TypeScript complains they “cannot be used as JSX,” it's almost always a @types/react version mismatch.

Style object typing

emails/styles.ts
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.

lib/email-schemas.ts
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.

Compile-time types (TypeScript)
  • Zero runtime cost — stripped during build
  • Full IDE autocomplete and refactoring support
  • Catches wrong prop types, missing fields, typos instantly
Runtime validation (Zod)
  • 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.


Key takeaway

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 interface types for every template's props — never any
  • Extend a BaseEmailProps type for shared fields like previewText
  • Use discriminated unions when one template handles multiple email variants
  • Type style objects with React.CSSProperties to catch CSS typos
  • Add @types/react overrides in package.json if you hit React 19 JSX component errors
  • Validate runtime data with Zod; infer types from schemas with z.infer
  • Use satisfies on preview props to keep them in sync with your type definitions
R

React Emails Pro

Team

Building production-ready email templates with React Email. Writing about transactional email best practices, deliverability, and developer tooling.

Production-ready templates

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates