Skip to content

Backend

React Email vs MJML — Building Branded Transactional Emails Without HTML Hell

Email HTML is tables and inline styles. React Email brings JSX and components to transactional templates. MJML is the markup-first alternative. We ship React Email on every Velocity build.

📧 ⚙️ 🎨

Email template markup is a time capsule. Tables for layout. Inline styles because external CSS is blocked. No Flexbox. No Grid. No CSS variables. Just raw 1990s HTML — the exact stack that nearly died in 2006 because nobody wanted to write email templates. Then React Email arrived (2023) and flipped it: write email templates in JSX, render to email-safe HTML, manage them in Git like normal code. MJML is the markup-first alternative — Pug-like syntax, compiles to responsive email HTML, no JavaScript runtime needed. Velocity X ships React Email by default because your email system should live in your repo, not a template builder dashboard. Here's the architecture, setup, 4 production templates, and when MJML actually wins.

Why Email HTML Is Broken

Outlook 2007 made a design choice: render HTML emails using Word's rendering engine instead of a real browser. This broke CSS floats, positioned elements, and media queries for 30% of email clients. Email designers had to respond: go back to tables, inline everything, use inline `style=""` attributes. Fast forward to 2026. Your email template is 200 lines of table soup, three colors hardcoded with `#ffffff` instead of a design token, and every CSS rule inlined so copy-pasting to a new template means duplicating 50 lines of boilerplate. You can't unit test it. You can't reuse a button component. You version it in a dashboard, not Git, so you lose audit history. When the designer wants to change the brand color from blue to purple, you manually edit 20 templates. This is the status quo for 95% of transactional email.

React Email fixes this. You write JSX. That JSX renders to inline-heavy, table-based HTML that every email client (including Outlook 2007) understands. But you get components, props, logic, type safety, tests, and Git history. Your button is a reusable <Button> component. Your brand color is a variable. Your template is 30 lines of readable code, not 200 lines of table soup.

React Email: JSX-First Templates

Setup is straightforward. Install `react-email` (npm/yarn), write templates as React components, send via Resend (or any email provider that accepts HTML).

npm install react-email @react-email/components

Now write your first template:

// lib/emails/WelcomeEmail.tsx
import React from 'react';
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Section,
  Text,
} from '@react-email/components';

const baseUrl = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : 'http://localhost:3000';

interface WelcomeEmailProps {
  userName: string;
  setupUrl: string;
  brandColor: string;
}

export const WelcomeEmail = ({
  userName,
  setupUrl,
  brandColor = '#6366f1',
}: WelcomeEmailProps) => (
  <Html lang="en">
    <Head />
    <Preview>Welcome to Velocity, {userName}</Preview>
    <Body style={{ backgroundColor: '#f3f4f6', fontFamily: 'sans-serif' }}>
      <Container style={{ backgroundColor: '#ffffff', padding: '40px 20px', borderRadius: '8px', maxWidth: '600px' }}>
        <Heading style={{ fontSize: '28px', fontWeight: 'bold', margin: '0 0 16px' }}>
          Welcome, {userName}
        </Heading>
        <Text style={{ fontSize: '16px', lineHeight: '1.5', color: '#374151', margin: '0 0 20px' }}>
          Your account is ready. Complete setup in 2 minutes to unlock all features.
        </Text>
        <Section style={{ marginTop: '30px', marginBottom: '30px' }}>
          <Button
            href={setupUrl}
            style={{
              backgroundColor: brandColor,
              color: '#ffffff',
              padding: '12px 24px',
              borderRadius: '6px',
              textDecoration: 'none',
              fontSize: '16px',
              fontWeight: 'bold',
              display: 'inline-block',
            }}
          >
            Start Setup
          </Button>
        </Section>
        <Text style={{ fontSize: '14px', lineHeight: '1.5', color: '#6b7280', margin: '20px 0 0' }}>
          Questions? Reply to this email or visit our{' '}
          <a href={`${baseUrl}/help`} style={{ color: brandColor, textDecoration: 'none' }}>
            help center
          </a>.
        </Text>
      </Container>
    </Body>
  </Html>
);

export default WelcomeEmail;

Key wins: types (TypeScript props), reusable styles (store theme colors in a constant), preview text, responsive container widths. Now compose this into a Resend send:

// netlify/functions/send-welcome.ts
import { Resend } from 'resend';
import { WelcomeEmail } from '../../lib/emails/WelcomeEmail';
import { getBrandColor } from '../../lib/brand';

const resend = new Resend(process.env.RESEND_API_KEY);

export default async (event: any) => {
  const { email, userName } = JSON.parse(event.body);

  try {
    const result = await resend.emails.send({
      from: 'Velocity <onboarding@velocity9.dev>',
      to: email,
      subject: `Welcome, ${userName}`,
      react: WelcomeEmail({
        userName,
        setupUrl: 'https://app.velocity9.dev/setup',
        brandColor: getBrandColor(), // Pull from brand.json
      }),
    });

    return {
      statusCode: 200,
      body: JSON.stringify({ success: true, messageId: result.data?.id }),
    };
  } catch (err: any) {
    console.error('Welcome email send failed:', err.message);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: err.message }),
    };
  }
};

Notice the pattern: templates are components, styled with inline objects (forces email-safe CSS), prop-driven (brand color from your config), version-controlled (Git history on every change), and testable. This is how modern email works.

MJML: Markup-First Alternative

MJML (Mailjet Markup Language) is the opposite philosophy. Instead of writing React, you write a mail-specific markup language that compiles to email-safe HTML. It's lower-level than React Email — you get built-in responsive components (mj-section, mj-column, mj-text) that handle Outlook compatibility for you. No JavaScript runtime. No build step beyond compilation.

<mjml>
  <mj-head>
    <mj-title>Welcome to Velocity</mj-title>
    <mj-preview>Welcome to Velocity, {userName}</mj-preview>
    <mj-style>
      .brand-color { color: #6366f1; }
    </mj-style>
    <mj-attributes>
      <mj-all font-family="sans-serif" />
      <mj-text font-size="16px" line-height="1.5" color="#374151" />
    </mj-attributes>
  </mj-head>
  <mj-body backgroundColor="#f3f4f6">
    <mj-section>
      <mj-column>
        <mj-text>
          <h1>Welcome, {userName}</h1>
        </mj-text>
        <mj-text>
          Your account is ready. Complete setup in 2 minutes to unlock all features.
        </mj-text>
        <mj-button href="https://app.velocity9.dev/setup" backgroundColor="#6366f1">
          Start Setup
        </mj-button>
        <mj-text font-size="14px" color="#6b7280">
          Questions? Reply to this email.
        </mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

MJML compiles this to email-safe HTML. No tables hardcoded — MJML handles the Outlook fallback for you. Responsive by default (mj-column handles mobile stacking). Lower barrier to entry (anyone can read this markup). But fewer power features: no loops (you can't map over an array of items), no conditional rendering based on user state, no TypeScript. For simple, static templates, MJML is simpler. For dynamic, component-driven email systems, React Email wins.

Four Essential Production Templates

Every SaaS ships these four emails. Here's how to build them in React Email:

1. Magic Link (Passwordless Auth)

// lib/emails/MagicLinkEmail.tsx
export const MagicLinkEmail = ({
  email,
  magicLink,
  brandColor,
}: {
  email: string;
  magicLink: string;
  brandColor: string;
}) => (
  <Html>
    <Preview>Your magic link is ready</Preview>
    <Body style={{ backgroundColor: '#f3f4f6', fontFamily: 'sans-serif' }}>
      <Container style={{ backgroundColor: '#ffffff', padding: '40px 20px', maxWidth: '600px', borderRadius: '8px' }}>
        <Heading style={{ fontSize: '20px', fontWeight: 'bold' }}>
          Click to sign in
        </Heading>
        <Text style={{ fontSize: '14px', color: '#666' }}>
          Or paste this link in your browser:
        </Text>
        <Section style={{ backgroundColor: '#f9fafb', padding: '16px', borderRadius: '6px', margin: '20px 0' }}>
          <Text style={{ fontSize: '12px', color: '#374151', wordBreak: 'break-all' }}>
            {magicLink}
          </Text>
        </Section>
        <Text style={{ fontSize: '12px', color: '#9ca3af' }}>
          This link expires in 24 hours. Only you should receive this email.
        </Text>
      </Container>
    </Body>
  </Html>
);

2. Receipt / Order Confirmation

// lib/emails/ReceiptEmail.tsx
interface LineItem {
  name: string;
  quantity: number;
  price: number;
}

export const ReceiptEmail = ({
  orderId,
  total,
  items,
  brandColor,
}: {
  orderId: string;
  total: number;
  items: LineItem[];
  brandColor: string;
}) => (
  <Html>
    <Preview>Your receipt for order {orderId}</Preview>
    <Body style={{ backgroundColor: '#f3f4f6' }}>
      <Container style={{ backgroundColor: '#ffffff', padding: '40px 20px' }}>
        <Heading style={{ color: brandColor }}>Order Confirmed</Heading>
        <Text>Order ID: {orderId}</Text>
        <Section style={{ margin: '20px 0', borderTop: '1px solid #e5e7eb', paddingTop: '20px' }}>
          {items.map((item) => (
            <div key={item.name} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '10px' }}>
              <Text style={{ margin: 0 }}>
                {item.name} × {item.quantity}
              </Text>
              <Text style={{ margin: 0, fontWeight: 'bold' }}>
                ${(item.price * item.quantity).toFixed(2)}
              </Text>
            </div>
          ))}
        </Section>
        <Section style={{ borderTop: '2px solid #e5e7eb', paddingTop: '20px', marginTop: '20px' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '18px', fontWeight: 'bold' }}>
            <Text style={{ margin: 0 }}>Total</Text>
            <Text style={{ margin: 0, color: brandColor }}>
              ${total.toFixed(2)}
            </Text>
          </div>
        </Section>
      </Container>
    </Body>
  </Html>
);

3. Weekly Digest

// lib/emails/WeeklyDigestEmail.tsx
interface DigestItem {
  title: string;
  description: string;
  link: string;
}

export const WeeklyDigestEmail = ({
  userName,
  items,
  brandColor,
}: {
  userName: string;
  items: DigestItem[];
  brandColor: string;
}) => (
  <Html>
    <Preview>Your weekly digest</Preview>
    <Body style={{ backgroundColor: '#f3f4f6' }}>
      <Container style={{ backgroundColor: '#ffffff', padding: '40px 20px' }}>
        <Heading>{userName}'s Weekly Digest</Heading>
        {items.map((item) => (
          <Section key={item.title} style={{ marginBottom: '20px', paddingBottom: '20px', borderBottom: '1px solid #e5e7eb' }}>
            <Heading as="h3" style={{ fontSize: '18px', color: brandColor }}>
              {item.title}
            </Heading>
            <Text>{item.description}</Text>
            <Button href={item.link} style={{ backgroundColor: brandColor, color: '#fff', padding: '8px 16px', borderRadius: '4px', textDecoration: 'none' }}>
              Read More
            </Button>
          </Section>
        ))}
      </Container>
    </Body>
  </Html>
);

4. Invoice / PDF Download

// lib/emails/InvoiceEmail.tsx
export const InvoiceEmail = ({
  invoiceNumber,
  total,
  downloadUrl,
  brandColor,
}: {
  invoiceNumber: string;
  total: number;
  downloadUrl: string;
  brandColor: string;
}) => (
  <Html>
    <Preview>Invoice {invoiceNumber}</Preview>
    <Body style={{ backgroundColor: '#f3f4f6' }}>
      <Container style={{ backgroundColor: '#ffffff', padding: '40px 20px' }}>
        <Heading>Invoice {invoiceNumber}</Heading>
        <Text style={{ fontSize: '28px', fontWeight: 'bold', margin: '20px 0' }}>
          ${total.toFixed(2)}
        </Text>
        <Button
          href={downloadUrl}
          style={{
            backgroundColor: brandColor,
            color: '#fff',
            padding: '12px 24px',
            borderRadius: '6px',
            textDecoration: 'none',
            fontSize: '16px',
            fontWeight: 'bold',
            display: 'inline-block',
          }}
        >
          Download Invoice
        </Button>
        <Text style={{ fontSize: '12px', color: '#9ca3af', marginTop: '20px' }}>
          This invoice expires in 30 days. Keep it for your records.
        </Text>
      </Container>
    </Body>
  </Html>
);

Six FAQs

Should I use React Email or MJML?

React Email if you're already JavaScript-forward (Next.js, Node.js backend, TypeScript). You get components, type safety, easy integration with your app state. MJML if you're building email templates in isolation — marketing or design teams who want readable markup without a JavaScript build. For SaaS with dynamic, data-driven emails, React Email wins.

How do I preview React Email templates locally?

React Email includes a preview server. Run `npx react-email dev`, then visit `http://localhost:3000`. You'll see a live preview of all templates with mock props. Edit your JSX, refresh, see changes immediately. Test before deploying.

Can I extract brand colors from brand.json?

Yes. Create a utility function: `export const getBrandColor = () => require('../brand.json').primaryColor`. Import it in your email templates. When brand.json updates, all emails inherit the new color. No hardcoded hex values.

Does React Email support unsubscribe links?

Yes, via Resend headers. Pass `headers: { 'List-Unsubscribe': '<${unsubscribeUrl}>' }` to the send call. Gmail and Outlook render an unsubscribe button. Legal requirement (CAN-SPAM, GDPR) — add it from day one.

How do I test email rendering across email clients?

Litmus or Email on Acid let you screenshot your template in 70+ clients (Gmail, Outlook, Apple Mail, etc.). Render your React Email template to HTML, paste into Litmus, and see client-by-client rendering. Takes 10 minutes, catches major layout breaks before shipping.

Can I send attachments with React Email?

Yes, via Resend. Pass `attachments: [{ filename: 'invoice.pdf', content: Buffer.from(...) }]`. Keep files small (Resend's 25MB limit per email). For large files, send a download link instead — cheaper and faster than attaching.

The Bottom Line

Email templates have lived in no-man's land for 20 years — too janky for code, too HTML-heavy for designers. React Email fixes it: write JSX, render to email-safe HTML, compose components, ship from your repo. MJML is simpler for static templates. But for modern SaaS — dynamic, branded, component-driven emails — React Email is the standard. Velocity X ships React Email templates on every build: welcome flows, magic links, receipts, digests, all prop-driven and brand-token-aware. Set up Resend (or any email provider accepting HTML), build your templates in React, and stop managing email in dashboards. Ready to integrate email into your SaaS stack? Check Aidxn Design SaaS builds for template scaffolding. For delivery and authentication setup, read Resend vs SendGrid vs Postmark.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.