Templates11 min read

Dark Mode Email Design: A Complete Guide for React Email Developers

Learn how every major email client handles dark mode and how to build React Email templates that look great in all of them. Includes code examples, color palettes, and a testing checklist.

R

React Emails Pro

March 23, 2026

TL;DR
  • Dark mode affects 80%+ of email recipients, but every email client handles it differently: Gmail inverts colors, Outlook applies its own dark palette, Apple Mail respects prefers-color-scheme
  • There are three rendering behaviors: no change, partial inversion, and full inversion. You need to design for all three.
  • React Email's component model makes dark mode easier than raw HTML, but you still need to know which CSS properties are safe to use
  • This guide covers every major email client, includes copy-paste React Email code, and ends with a testing checklist

How email clients handle dark mode

The core problem with dark mode in email is that there is no standard. Web browsers all respect prefers-color-scheme the same way. Email clients do not. Some ignore your dark mode styles entirely. Some rewrite your colors without asking. Some let you control everything. You need to know which group each client falls into before writing a single line of CSS.

Email ClientBehaviorRespects Your CSS?Notes
Apple Mail (macOS/iOS)Full supportYesReads prefers-color-scheme media query
Gmail (iOS)Partial inversionNoAuto-inverts light backgrounds to dark
Gmail (Android)Partial inversionNoSame auto-inversion, inconsistent across devices
Gmail (web)No dark modeN/AAlways renders light theme
Outlook (Windows)Full inversionNoInverts everything including images with light backgrounds
Outlook (macOS)Full supportYesUses prefers-color-scheme like Apple Mail
Outlook.com (web)Partial inversionPartialInverts backgrounds, sometimes ignores overrides
Yahoo MailPartial inversionNoRewrites background colors unpredictably
Samsung MailFull supportYesReads media query when available
ThunderbirdFull supportYesStandard prefers-color-scheme support

Dark mode behavior across major email clients (2026)

Gmail strips <style> blocks on some clients, which means your prefers-color-scheme media query never runs. Inline styles are your only guaranteed fallback.

The three rendering strategies

Every email you send will encounter one of three dark mode behaviors. Your design needs to survive all of them without looking broken.

1. No change (Gmail web, older clients)

The client ignores dark mode entirely. Your email renders exactly as designed. This is the easiest case: just make sure your light theme looks good.

2. Partial inversion (Gmail mobile, Yahoo, Outlook.com)

The client detects light backgrounds and flips them to dark. Light text on dark backgrounds stays untouched. This is where most rendering issues happen: the client changes your background from white to dark gray, but if your text is already a medium gray, it becomes unreadable.

The partial inversion trap

If you use #666666 text on a #ffffff background, partial inversion flips the background to #1a1a1a but leaves the text at #666666. The contrast ratio drops from 5.7:1 to 2.6:1, failing WCAG AA. Always use high-contrast text colors: black or near-black on light, white or near-white on dark.

3. Full support (Apple Mail, Samsung Mail, Outlook macOS)

The client reads your prefers-color-scheme: dark media query and applies your custom dark mode styles. This gives you full control, but only works for the subset of clients that support it.


Dark mode setup in React Email

React Email components render to email-safe HTML with inline styles. To add dark mode, you use the <Head> component to inject a <style> block with your media query, then use utility classes or data attributes that the query targets.

emails/dark-mode-base.tsx
import {
  Html, Head, Body, Container, Section,
  Text, Img, Hr, Link,
} from "@react-email/components";

const darkModeStyles = `
  :root {
    color-scheme: light dark;
  }

  @media (prefers-color-scheme: dark) {
    .dark-bg { background-color: #1a1a2e !important; }
    .dark-bg-card { background-color: #16213e !important; }
    .dark-text { color: #e2e8f0 !important; }
    .dark-text-muted { color: #94a3b8 !important; }
    .dark-border { border-color: #334155 !important; }
    .dark-img-invert { filter: brightness(0) invert(1) !important; }
  }
`;

interface WelcomeEmailProps {
  name: string;
  loginUrl: string;
}

export default function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head>
        <style dangerouslySetInnerHTML={{ __html: darkModeStyles }} />
      </Head>
      <Body
        className="dark-bg"
        style={{
          backgroundColor: "#ffffff",
          fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
          margin: "0",
          padding: "0",
        }}
      >
        <Container
          className="dark-bg-card"
          style={{
            backgroundColor: "#f8fafc",
            maxWidth: "600px",
            margin: "0 auto",
            padding: "40px 20px",
          }}
        >
          <Text
            className="dark-text"
            style={{ color: "#0f172a", fontSize: "24px", fontWeight: "bold" }}
          >
            Welcome, {name}
          </Text>
          <Text
            className="dark-text-muted"
            style={{ color: "#475569", fontSize: "16px", lineHeight: "1.6" }}
          >
            Your account is ready. Click below to sign in and get started.
          </Text>
          <Link
            href={loginUrl}
            style={{
              backgroundColor: "#f59e0b",
              color: "#0f172a",
              padding: "12px 24px",
              borderRadius: "6px",
              textDecoration: "none",
              fontWeight: "600",
              display: "inline-block",
            }}
          >
            Sign in to your account
          </Link>
        </Container>
      </Body>
    </Html>
  );
}
The color-scheme: light dark declaration in :root tells supporting clients that your email is dark-mode-aware. Without it, some clients skip your media query entirely.

Building a dark-mode-safe color palette

The biggest mistake developers make is picking colors that only work in one mode. You need a palette where every color has a light variant and a dark variant, and both pass WCAG AA contrast requirements.

RoleLight ModeDark ModeContrast (Light)Contrast (Dark)
Body background#ffffff#1a1a2e--
Card background#f8fafc#16213e--
Primary text#0f172a#e2e8f015.4:112.1:1
Secondary text#475569#94a3b87.1:15.4:1
Border#e2e8f0#334155--
CTA button#f59e0b#f59e0b2.1:1 (dark text)2.1:1 (dark text)
CTA text#0f172a#0f172a--
Link color#2563eb#60a5fa4.6:15.2:1

Recommended dual-mode color palette

Keep your CTA button the same color in both modes. A consistent button color anchors the reader's eye regardless of their theme. Use dark text on the button in both modes to maintain contrast.

Handling images and logos

Images are the most common dark mode failure point. A logo with a transparent background looks great on white but vanishes on a dark background. Screenshots with white borders create harsh boxes. Product images with white backgrounds look disconnected.

Safe image practices
  • Use transparent PNGs with colors that work on both light and dark backgrounds
  • Add a subtle border-radius and padding to product images
  • Provide separate light/dark logo variants using the CSS class swap technique
  • Use SVG logos where possible (they scale and can be color-swapped)
  • Test every image on a dark background before sending
Common mistakes
  • Transparent logos that are black or very dark (invisible on dark backgrounds)
  • Screenshots with white backgrounds and no border
  • Using CSS filter: invert() on photos (turns everything negative)
  • Forgetting that Outlook inverts transparent PNG backgrounds to black
  • JPEG logos (no transparency, always shows a white rectangle)

The logo swap technique uses two image elements with classes that toggle visibility based on the color scheme:

emails/components/logo.tsx
const logoSwapStyles = `
  @media (prefers-color-scheme: dark) {
    .logo-light { display: none !important; }
    .logo-dark { display: block !important; }
  }
`;

// In your email component:
<Head>
  <style dangerouslySetInnerHTML={{ __html: logoSwapStyles }} />
</Head>

{/* Light mode logo (default) */}
<Img
  className="logo-light"
  src="https://your-cdn.com/logo-dark-text.png"
  alt="YourApp"
  width={120}
  height={32}
  style={{ display: "block" }}
/>

{/* Dark mode logo (hidden by default, shown in dark mode) */}
<Img
  className="logo-dark"
  src="https://your-cdn.com/logo-light-text.png"
  alt="YourApp"
  width={120}
  height={32}
  style={{ display: "none" }}
/>
This technique only works in clients that support prefers-color-scheme. In Gmail and Yahoo, both images may show or the light logo may appear on a dark background. As a fallback, choose a logo color that has reasonable contrast on both black and white backgrounds.

Outlook-specific dark mode fixes

Outlook on Windows is the hardest email client for dark mode. It uses Word's rendering engine, ignores prefers-color-scheme, and applies its own color transformations. There are three Outlook dark mode settings users can choose, and you cannot detect which one is active.

Outlook SettingWhat it doesYour control
Don't change message colorsNo dark mode appliedFull control (light theme renders as-is)
Use system settings (default)Inverts light backgrounds, adjusts textVery limited - can only protect specific elements
Always use dark backgroundForces dark background on everythingMinimal - Outlook overrides most inline styles

The only reliable defense against Outlook dark mode is the [data-ogsc] and [data-ogsb] attribute selectors. Outlook adds these attributes when it applies dark mode, and you can use them to override its color choices:

outlook-dark-mode-overrides.css
/* Outlook dark mode overrides */
/* [data-ogsc] targets Outlook's dark mode text color changes */
/* [data-ogsb] targets Outlook's dark mode background changes */

[data-ogsc] .outlook-text-white {
  color: #ffffff !important;
}

[data-ogsc] .outlook-text-dark {
  color: #0f172a !important;
}

[data-ogsb] .outlook-bg-dark {
  background-color: #1a1a2e !important;
}

[data-ogsb] .outlook-bg-card {
  background-color: #16213e !important;
}

/* Prevent Outlook from inverting your CTA button */
[data-ogsb] .outlook-btn-preserve {
  background-color: #f59e0b !important;
}

[data-ogsc] .outlook-btn-preserve {
  color: #0f172a !important;
}
The data-ogsc and data-ogsb selectors are Outlook-specific and undocumented by Microsoft. They work reliably in Outlook 2019+ and Microsoft 365 desktop clients. Test before relying on them for older Outlook versions.

Full dark mode email component

Here is a complete React Email component that handles all three dark mode strategies: custom styles for supporting clients, resilient defaults for auto-inverting clients, and Outlook-specific overrides.

emails/password-reset-dark-mode.tsx
import {
  Html, Head, Body, Container, Section,
  Text, Link, Hr, Img, Preview,
} from "@react-email/components";

const styles = `
  :root { color-scheme: light dark; }

  @media (prefers-color-scheme: dark) {
    .body-bg { background-color: #0f172a !important; }
    .container-bg { background-color: #1e293b !important; }
    .heading { color: #f1f5f9 !important; }
    .body-text { color: #cbd5e1 !important; }
    .muted-text { color: #64748b !important; }
    .divider { border-color: #334155 !important; }
    .footer-text { color: #64748b !important; }
  }

  /* Outlook dark mode overrides */
  [data-ogsc] .heading { color: #f1f5f9 !important; }
  [data-ogsc] .body-text { color: #cbd5e1 !important; }
  [data-ogsb] .body-bg { background-color: #0f172a !important; }
  [data-ogsb] .container-bg { background-color: #1e293b !important; }
  [data-ogsb] .btn-preserve { background-color: #f59e0b !important; }
  [data-ogsc] .btn-preserve { color: #0f172a !important; }
`;

interface PasswordResetProps {
  name: string;
  resetUrl: string;
  expiresIn: string;
}

export default function PasswordResetEmail({
  name,
  resetUrl,
  expiresIn = "1 hour",
}: PasswordResetProps) {
  return (
    <Html>
      <Head>
        <style dangerouslySetInnerHTML={{ __html: styles }} />
      </Head>
      <Preview>Reset your password - link expires in {expiresIn}</Preview>
      <Body
        className="body-bg"
        style={{
          backgroundColor: "#f1f5f9",
          margin: "0",
          padding: "40px 0",
          fontFamily:
            "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
        }}
      >
        <Container
          className="container-bg"
          style={{
            backgroundColor: "#ffffff",
            maxWidth: "560px",
            margin: "0 auto",
            borderRadius: "8px",
            padding: "40px 32px",
          }}
        >
          <Text
            className="heading"
            style={{
              color: "#0f172a",
              fontSize: "22px",
              fontWeight: "600",
              margin: "0 0 8px",
            }}
          >
            Reset your password
          </Text>
          <Text
            className="body-text"
            style={{
              color: "#334155",
              fontSize: "15px",
              lineHeight: "1.6",
              margin: "0 0 24px",
            }}
          >
            Hi {name}, we received a request to reset your password. Click the
            button below to choose a new one. This link expires in {expiresIn}.
          </Text>
          <Link
            className="btn-preserve"
            href={resetUrl}
            style={{
              backgroundColor: "#f59e0b",
              color: "#0f172a",
              padding: "12px 32px",
              borderRadius: "6px",
              textDecoration: "none",
              fontWeight: "600",
              fontSize: "15px",
              display: "inline-block",
            }}
          >
            Reset password
          </Link>
          <Hr
            className="divider"
            style={{ borderColor: "#e2e8f0", margin: "32px 0" }}
          />
          <Text
            className="footer-text"
            style={{
              color: "#94a3b8",
              fontSize: "13px",
              lineHeight: "1.5",
              margin: "0",
            }}
          >
            If you didn&apos;t request this, you can safely ignore this email.
            Your password won&apos;t change until you click the link above.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

Testing checklist

Dark mode rendering bugs are invisible until someone reports them. Use this checklist every time you ship a new email template or update an existing one.

  • Apple Mail (macOS): Toggle system dark mode and verify your prefers-color-scheme styles apply
  • Gmail app (iOS + Android): Send a test email, enable dark mode on the device, and check that text remains readable after auto-inversion
  • Outlook desktop(Windows): Test with all three dark mode settings (don't change, system settings, always dark)
  • Outlook.com: Check the web client in dark mode for background inversion issues
  • Logo visibility: Verify your logo is visible on both white and dark backgrounds
  • CTA button: Confirm the button color and text remain readable and prominent in dark mode
  • Images: Check that no images have transparent backgrounds that disappear on dark surfaces
  • Text contrast: Verify all text passes WCAG AA (4.5:1 for body text, 3:1 for large text) in both modes
  • Borders and dividers: Confirm borders are visible in dark mode (light gray borders vanish on dark backgrounds)
  • Links: Check that link colors have enough contrast against the dark background and are distinguishable from body text

Testing tools

Litmus and Email on Acidboth offer dark mode previews across clients. If you don't have a paid tool, send test emails to real accounts: a Gmail account on an Android phone, an iCloud account on a Mac, and an Outlook.com account on Windows covers the three major rendering behaviors.


Common pitfalls and how to avoid them

PitfallWhat happensFix
Using only hex white (#ffffff) backgroundsAuto-inverting clients flip to near-black, but your medium-gray text becomes unreadableUse near-black (#0f172a) text on light backgrounds so inverted contrast stays high
CSS filter: invert(1) on logosWorks in browsers but most email clients strip the filter propertyUse the logo swap technique with two separate images
Relying on <style> blocks aloneGmail strips <style> on many clients, your dark mode never activatesDesign your light theme to survive inversion. Dark mode CSS is an enhancement, not a requirement
Transparent PNGs with dark contentContent disappears when the background is inverted to darkAdd a visible border or use a padded container with a fixed background color
Forgetting color-scheme: light darkSupporting clients skip your media queryAlways declare color-scheme in :root
Same border color in both modesLight gray borders vanish on dark backgroundsUse separate border colors in your dark mode styles

Dark mode email pitfalls and solutions


Key takeaway
  • Declare color-scheme: light darkin your email's :root to signal dark mode support
  • Design your light theme with high-contrast text so auto-inversion (Gmail, Yahoo) doesn't break readability
  • Use prefers-color-scheme: dark media queries for clients that support them (Apple Mail, Samsung Mail)
  • Add [data-ogsc] and [data-ogsb] overrides for Outlook desktop
  • Swap logos using two images with display toggling, not CSS filters
  • Test on real devices: Apple Mail, Gmail mobile, and Outlook Windows cover the three major rendering strategies
  • Keep CTA buttons a consistent, high-contrast color in both modes
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