- 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 Client | Behavior | Respects Your CSS? | Notes |
|---|---|---|---|
| Apple Mail (macOS/iOS) | Full support | Yes | Reads prefers-color-scheme media query |
| Gmail (iOS) | Partial inversion | No | Auto-inverts light backgrounds to dark |
| Gmail (Android) | Partial inversion | No | Same auto-inversion, inconsistent across devices |
| Gmail (web) | No dark mode | N/A | Always renders light theme |
| Outlook (Windows) | Full inversion | No | Inverts everything including images with light backgrounds |
| Outlook (macOS) | Full support | Yes | Uses prefers-color-scheme like Apple Mail |
| Outlook.com (web) | Partial inversion | Partial | Inverts backgrounds, sometimes ignores overrides |
| Yahoo Mail | Partial inversion | No | Rewrites background colors unpredictably |
| Samsung Mail | Full support | Yes | Reads media query when available |
| Thunderbird | Full support | Yes | Standard prefers-color-scheme support |
Dark mode behavior across major email clients (2026)
<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.
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>
);
}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.
| Role | Light Mode | Dark Mode | Contrast (Light) | Contrast (Dark) |
|---|---|---|---|---|
| Body background | #ffffff | #1a1a2e | - | - |
| Card background | #f8fafc | #16213e | - | - |
| Primary text | #0f172a | #e2e8f0 | 15.4:1 | 12.1:1 |
| Secondary text | #475569 | #94a3b8 | 7.1:1 | 5.4:1 |
| Border | #e2e8f0 | #334155 | - | - |
| CTA button | #f59e0b | #f59e0b | 2.1:1 (dark text) | 2.1:1 (dark text) |
| CTA text | #0f172a | #0f172a | - | - |
| Link color | #2563eb | #60a5fa | 4.6:1 | 5.2:1 |
Recommended dual-mode color palette
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.
- 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
- 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:
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" }}
/>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 Setting | What it does | Your control |
|---|---|---|
| Don't change message colors | No dark mode applied | Full control (light theme renders as-is) |
| Use system settings (default) | Inverts light backgrounds, adjusts text | Very limited - can only protect specific elements |
| Always use dark background | Forces dark background on everything | Minimal - 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 */
/* [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;
}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.
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't request this, you can safely ignore this email.
Your password won'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-schemestyles 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
| Pitfall | What happens | Fix |
|---|---|---|
| Using only hex white (#ffffff) backgrounds | Auto-inverting clients flip to near-black, but your medium-gray text becomes unreadable | Use near-black (#0f172a) text on light backgrounds so inverted contrast stays high |
| CSS filter: invert(1) on logos | Works in browsers but most email clients strip the filter property | Use the logo swap technique with two separate images |
| Relying on <style> blocks alone | Gmail strips <style> on many clients, your dark mode never activates | Design your light theme to survive inversion. Dark mode CSS is an enhancement, not a requirement |
| Transparent PNGs with dark content | Content disappears when the background is inverted to dark | Add a visible border or use a padded container with a fixed background color |
| Forgetting color-scheme: light dark | Supporting clients skip your media query | Always declare color-scheme in :root |
| Same border color in both modes | Light gray borders vanish on dark backgrounds | Use separate border colors in your dark mode styles |
Dark mode email pitfalls and solutions
- Declare
color-scheme: light darkin your email's:rootto 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: darkmedia 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