React Email14 min read

React Email Design Tokens & Theming: Build Consistent Email UIs That Scale

Stop copy-pasting button styles and hex codes. Use design tokens to centralize colors, spacing, typography, and layout across all your React Email templates. Includes multi-brand support and dark mode patterns.

R

React Emails Pro

March 4, 2026

Your welcome email uses one shade of blue. Your password reset uses another. Your invoice uses a third. They all look vaguely blue, but none of them match.

After your tenth template, you're copy-pasting button styles and hoping for the best. When the brand changes, you grep for hex codes and pray you didn't miss any.

Design tokens fix this: centralized values for colors, spacing, typography, and layout that give you consistency without the copy-paste chaos.

What are design tokens (and why email needs them)

Design tokens are the named constants that define your visual language: colors, spacing, font sizes, border radii, shadows.

In web apps, you have CSS variables, Tailwind config, or design system packages. In React Email, you need something that:

  • Works across all email clients (no CSS vars in Outlook)
  • Keeps templates consistent without copy-paste
  • Makes brand changes a one-line update
  • Supports light/dark mode where possible

The answer: a plain TypeScript object that you import into every template.


Pattern 1: Basic design tokens file

Start with a single file that exports your brand values:

emails/lib/design-tokens.ts
export const tokens = {
  colors: {
    primary: "#3B82F6",
    primaryDark: "#2563EB",
    secondary: "#10B981",
    text: "#1F2937",
    textLight: "#6B7280",
    background: "#FFFFFF",
    backgroundAlt: "#F9FAFB",
    border: "#E5E7EB",
    error: "#EF4444",
    success: "#10B981",
  },
  spacing: {
    xs: "8px",
    sm: "12px",
    md: "16px",
    lg: "24px",
    xl: "32px",
    xxl: "48px",
  },
  typography: {
    fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
    fontSize: {
      xs: "12px",
      sm: "14px",
      base: "16px",
      lg: "18px",
      xl: "20px",
      xxl: "24px",
    },
    lineHeight: {
      tight: "1.25",
      normal: "1.5",
      relaxed: "1.75",
    },
    fontWeight: {
      normal: "400",
      medium: "500",
      semibold: "600",
      bold: "700",
    },
  },
  borderRadius: {
    sm: "4px",
    md: "6px",
    lg: "8px",
    full: "9999px",
  },
  layout: {
    maxWidth: "600px",
    contentPadding: "24px",
  },
};

Now use these tokens in your templates:

emails/welcome.tsx
import { tokens } from "./lib/design-tokens";

export default function WelcomeEmail({ name }: { name: string }) {
  return (
    <Html>
      <Body style={{ backgroundColor: tokens.colors.background }}>
        <Container style={{ maxWidth: tokens.layout.maxWidth }}>
          <Heading
            style={{
              color: tokens.colors.text,
              fontSize: tokens.typography.fontSize.xl,
              fontFamily: tokens.typography.fontFamily,
            }}
          >
            Welcome, {name}!
          </Heading>
          <Button
            href="https://app.example.com/onboarding"
            style={{
              backgroundColor: tokens.colors.primary,
              color: "#FFFFFF",
              padding: `${tokens.spacing.md} ${tokens.spacing.lg}`,
              borderRadius: tokens.borderRadius.md,
              fontWeight: tokens.typography.fontWeight.semibold,
            }}
          >
            Get Started
          </Button>
        </Container>
      </Body>
    </Html>
  );
}
When the brand updates, you change tokens.colors.primary once and every template updates automatically.

Pattern 2: Component-level token helpers

Typing out tokens.spacing.md everywhere gets verbose. Create helper functions that encapsulate common patterns:

emails/lib/button-styles.ts
import { tokens } from "./design-tokens";

export const buttonStyles = {
  primary: {
    backgroundColor: tokens.colors.primary,
    color: "#FFFFFF",
    padding: `${tokens.spacing.md} ${tokens.spacing.lg}`,
    borderRadius: tokens.borderRadius.md,
    fontWeight: tokens.typography.fontWeight.semibold,
    fontSize: tokens.typography.fontSize.base,
    textDecoration: "none",
    display: "inline-block",
  },
  secondary: {
    backgroundColor: tokens.colors.backgroundAlt,
    color: tokens.colors.text,
    padding: `${tokens.spacing.md} ${tokens.spacing.lg}`,
    borderRadius: tokens.borderRadius.md,
    fontWeight: tokens.typography.fontWeight.medium,
    fontSize: tokens.typography.fontSize.base,
    textDecoration: "none",
    display: "inline-block",
    border: `1px solid ${tokens.colors.border}`,
  },
  ghost: {
    backgroundColor: "transparent",
    color: tokens.colors.primary,
    padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,
    borderRadius: tokens.borderRadius.md,
    fontWeight: tokens.typography.fontWeight.medium,
    fontSize: tokens.typography.fontSize.sm,
    textDecoration: "underline",
  },
};

Now your button usage is cleaner:

emails/password-reset.tsx
import { buttonStyles } from "./lib/button-styles";

<Button
  href={resetUrl}
  style={buttonStyles.primary}
>
  Reset Password
</Button>

Pattern 3: Semantic tokens (not just colors)

Don't just expose colors.blue500. Use semantic names that describe intent:

emails/lib/design-tokens.ts
export const tokens = {
  colors: {
    // Semantic colors (intent-based)
    brand: "#3B82F6",
    brandHover: "#2563EB",
    textPrimary: "#1F2937",
    textSecondary: "#6B7280",
    textInverse: "#FFFFFF",
    bgPrimary: "#FFFFFF",
    bgSecondary: "#F9FAFB",
    bgInverse: "#1F2937",
    borderDefault: "#E5E7EB",
    borderFocus: "#3B82F6",
    
    // Feedback colors
    success: "#10B981",
    error: "#EF4444",
    warning: "#F59E0B",
    info: "#3B82F6",
  },
  // ...
};

This makes code more readable and survives brand redesigns:

  • textPrimary is clearer than gray900
  • brand adapts when you rebrand
  • success doesn't need to be green forever

Common mistakes to avoid

1) Using CSS variables in email

CSS custom properties (--primary-color) don't work in Outlook or many mobile clients. Stick to inline styles with static values from your tokens object.

color: var(--primary) breaks in Outlook
color: tokens.colors.primary works everywhere

2) Over-engineering tokens before you have 5 templates

Don't build a design system for 2 emails. Start with a simple tokens file. Add structure when you feel the pain of duplication.

3) No fallback values

Always provide safe defaults for optional props:

emails/lib/button-styles.ts
export function getButtonStyle(variant: "primary" | "secondary" = "primary") {
  const styles = {
    primary: buttonStyles.primary,
    secondary: buttonStyles.secondary,
  };
  return styles[variant] ?? styles.primary; // Fallback to primary
}

4) Hardcoding spacing in templates

If you write padding: "20px" in 10 places, you'll regret it when the design changes. Use spacing tokens everywhere.


When to use design tokens

Design tokens make sense when:

  • You have 5+ email templates
  • Your brand changes occasionally (color updates, font tweaks)
  • Multiple developers work on email templates
  • You support multi-brand or white-label products

Skip tokens if:

  • You have 1-2 simple templates and no plans to expand
  • Your brand is locked and never changes
  • You're the only developer and can remember the colors

For most SaaS products, the break-even point is around 3-5 templates. After that, tokens save time and prevent bugs.


Next steps

If you want production-ready templates with design tokens already baked in, check out:

For error handling and validation that complements your token system, see Email prop validation patterns.

Tiny win: Add a README.md in your emails/lib/ folder that documents your token naming conventions. Future you (and your teammates) will thank you.

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