Templates11 min read

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

Build React Email dark-mode styles with readable defaults. Compare client behavior, handle logos, check contrast, and test received messages in target inboxes.

R

React Emails Pro

March 23, 2026 ยท Updated September 24, 2026

TL;DR
  • Email clients handle dark mode differently. Gmail apps can change colors automatically, Outlook behavior varies by app, and supporting clients can use 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

Debugging a message that looks right locally but breaks in Gmail? Use the Gmail dark-mode troubleshooting reference for a smaller reproduction, logo checks, and a contrast utility.


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.

Client familyWhat to checkDesign approach
Apple MailWhether your color-scheme query appliesProvide a custom palette and readable defaults
Gmail appsAutomatic color changes on each platformTest default colors without depending on a dark query
Gmail webThe received message in the active themeCheck separately from the mobile apps
Classic Outlook for WindowsMessage background settings and renderingTest separately from new Outlook
Outlook.com and other Outlook appsClient-specific color changes and attributesVerify overrides in the exact target app

A testing map, not a guarantee for every version in a client family.

Check Can I email's feature tests for individual versions and test dates before choosing which clients need custom rules.

Gmail supports style blocks, but its documented media queries do not include prefers-color-scheme. Give the default markup explicit colors and inspect the delivered result. Inline styles can still be changed by the receiving app.


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. Original message colors

Some viewing configurations leave the message colors unchanged. Check that the original palette remains readable inside the surrounding interface, and verify it separately from mobile apps.

2. Automatic color changes

An app may transform foregrounds and backgrounds differently. Inspect each text and background pair after delivery instead of assuming a predictable inversion formula. A readable source palette is only the starting point.

Check the pair that actually renders

A darkened surface can leave muted text difficult to read. Measure the displayed foreground against its background, including button labels and small-print instructions. Do not infer the result from the original hex values alone.

3. A custom palette in supporting clients

The client reads your prefers-color-scheme: dark media query and applies your custom dark mode styles. This lets you supply a second palette where the query is supported. Check the exact client version before relying on 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. It does not force unsupported clients to apply the query or prevent automatic color changes.

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#e2e8f017.06:112.89:1
Secondary text#475569#94a3b87.24:16.20:1
Border#e2e8f0#334155--
CTA button#f59e0b#f59e0b8.31:1 (dark text)8.31:1 (dark text)
CTA text#0f172a#0f172a--
Link color#2563eb#60a5fa4.94:16.25:1

Text ratios use the card background in each mode; button ratios use the button background. These are source colors before client transformations.

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
  • Check image-format support before replacing a raster logo with an SVG
  • 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)
  • Assuming transparent pixels will always sit on a light background
  • 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

Test classic Outlook for Windows separately from new Outlook, Outlook.com, and the mobile apps. Their behavior is not interchangeable. Microsoft's dark-mode instructions distinguish these experiences and explain the controls for changing a message background.

Selectors such as [data-ogsc] and [data-ogsb] target attributes observed in some Outlook clients. The following CSS is a candidate override for a client where you have confirmed those attributes. It is not a general fix for classic Windows Outlook.

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;
}

/* Candidate button overrides where these attributes are present */
[data-ogsb] .outlook-btn-preserve {
  background-color: #f59e0b !important;
}

[data-ogsc] .outlook-btn-preserve {
  color: #0f172a !important;
}
The data-ogsc and data-ogsb attributes are described in Can I email's client notes. Check for them in your target app; do not assume a successful Outlook.com test covers Windows desktop.

Full dark mode email component

Here is a React Email starting point with default colors and a custom palette for supporting clients. Test the received HTML after replacing its assets, links, and branding.

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: #94a3b8 !important; }
    .divider { border-color: #334155 !important; }
    .footer-text { color: #94a3b8 !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 the message background controls available in your installed version
  • 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 unreadableStart with strong contrast and inspect the transformed pair after delivery
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 aloneAn unsupported color-scheme query does not applyDesign 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 darkConsider exporting a contrasting background as part of the image itself
Forgetting color-scheme: light darkThe document does not declare the intended color schemesAlways 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 only in Outlook clients where those attributes are present
  • 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