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.
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:
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:
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>
);
}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:
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:
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:
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:
textPrimaryis clearer thangray900brandadapts when you rebrandsuccessdoesn'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 everywhere2) 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:
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:
- React Email templates for SaaS — full library with token-based styling
- Transactional templates for Next.js — core flows you can customize
- Composable React Email patterns — component architecture that scales
For error handling and validation that complements your token system, see Email prop validation patterns.
README.md in your emails/lib/ folder that documents your token naming conventions. Future you (and your teammates) will thank you.