- 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 family | What to check | Design approach |
|---|---|---|
| Apple Mail | Whether your color-scheme query applies | Provide a custom palette and readable defaults |
| Gmail apps | Automatic color changes on each platform | Test default colors without depending on a dark query |
| Gmail web | The received message in the active theme | Check separately from the mobile apps |
| Classic Outlook for Windows | Message background settings and rendering | Test separately from new Outlook |
| Outlook.com and other Outlook apps | Client-specific color changes and attributes | Verify 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.
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. 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.
| Role | Light Mode | Dark Mode | Contrast (Light) | Contrast (Dark) |
|---|---|---|---|---|
| Body background | #ffffff | #1a1a2e | - | - |
| Card background | #f8fafc | #16213e | - | - |
| Primary text | #0f172a | #e2e8f0 | 17.06:1 | 12.89:1 |
| Secondary text | #475569 | #94a3b8 | 7.24:1 | 6.20:1 |
| Border | #e2e8f0 | #334155 | - | - |
| CTA button | #f59e0b | #f59e0b | 8.31:1 (dark text) | 8.31:1 (dark text) |
| CTA text | #0f172a | #0f172a | - | - |
| Link color | #2563eb | #60a5fa | 4.94:1 | 6.25:1 |
Text ratios use the card background in each mode; button ratios use the button background. These are source colors before client transformations.
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
- Check image-format support before replacing a raster logo with an SVG
- 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)
- 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:
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
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 */
/* [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;
}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.
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'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 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
| Pitfall | What happens | Fix |
|---|---|---|
| Using only hex white (#ffffff) backgrounds | Auto-inverting clients flip to near-black, but your medium-gray text becomes unreadable | Start with strong contrast and inspect the transformed pair after delivery |
| 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 | An unsupported color-scheme query does not apply | 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 | Consider exporting a contrasting background as part of the image itself |
| Forgetting color-scheme: light dark | The document does not declare the intended color schemes | 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 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