If your React Email looks right in preview but breaks in Gmail dark mode, start with the received HTML and the exact Gmail app. A browser preview cannot reproduce every change an inbox makes. Check the logo, button label, and secondary text separately before changing the whole template.
| What you see | First thing to inspect | Useful next move |
|---|---|---|
| Dark CSS works in preview only | Whether Gmail supports the media query | Make the default styles readable without that query |
| Logo disappears | Transparent pixels around a dark wordmark | Export a logo with its own contrasting background |
| Button is visible, label is not | Text and background changing independently | Set both colors on the link and test a simpler pair |
| Only the sent email breaks | HTML after rendering and provider processing | Compare the received source with the local output |
| One phone fails, another passes | App, OS, account type, and theme settings | Keep separate screenshots and test records |
A troubleshooting order, not a promise that one CSS fix works in every inbox.
A disappearing password-reset button is worth fixing before a slightly different shade of gray. That sounds obvious until a dark-mode bug sends you through an afternoon of selectors. Work from the action the reader needs to take, then check the supporting details. A successful repair leaves the email readable in light mode too.
This reference is for a specific failure: an email that renders correctly locally and changes inside Gmail. For the initial template structure, use the React Email dark-mode design guide. Here, we will isolate the failure, build a small reproduction, and decide whether a client-specific workaround is worth keeping.
Why your dark-mode media query does not fix Gmail
React Email produces HTML. The receiving app decides which styles to accept and how to display the result. Moving a declaration into a React component does not give it extra support in an inbox.
Google's Gmail CSS reference lists media queries for width, orientation, and resolution. It does not list prefers-color-scheme. It also documents support for style blocks, so "Gmail removes all CSS" is the wrong diagnosis. A supported width query and an unsupported color-scheme query can behave differently in the same email.
Keep your dark-theme rules for clients that use them. Give the default HTML an explicit background and text color, then inspect what Gmail does to that pair. Adding more !important declarations to a query the client does not apply will not make it run.
A color-scheme meta tag is not a universal opt-out from inbox color changes. Neither a meta tag nor a dark preview toggle proves that your chosen palette will survive in a received message.
Can I email's support table is a useful starting point for choosing test clients. Read its dates and client notes: the page lists March 8, 2023 as its last full feature test, with later entries for some clients. Treat it as evidence about those tests, not a fresh certification of every installed app version.
Record which Gmail you mean
"Gmail dark mode is broken" is not enough information for a reproducible bug. Record the operating system, Gmail app version, whether the account is a Google account or another provider, and the active app and system themes. Include the subject of the test message so nobody accidentally compares an old send with a new preview.
Open the same message in Gmail web and the failing phone app. If the source is the same but the result differs, you have narrowed the problem to the receiving environment. If the messages came from different test sends, first rule out different assets or template revisions. Keep one variable changing at a time.
Avoid using a forwarded copy as the reproduction. Forwarding can wrap the original message in new markup, and the recipient may open it in a different account or app. Ask for the original message source and a screenshot with sensitive details removed. Send your revised fixture directly to the test account so the two versions follow the same path.
Build a small reproduction with real text
Reduce the failing template to a heading, a paragraph, a button, and a footer. Preserve the colors and wording of the failing elements. A generic lorem ipsum block will miss a wrapped button label or an account address that pushes the layout wider.
The following fixture uses React Email 6 imports. It deliberately has no dark-mode override: first find out what happens to the base colors. The example URL is a harmless placeholder; replace it with a staging destination you control before testing navigation. Do not put a live password-reset token in a shared screenshot or fixture.
import * as React from "react";
import {
Html, Head, Preview, Body, Container, Text, Button,
} from "react-email";
export interface GmailColorProbeProps {
actionUrl: string;
}
export default function GmailColorProbe({ actionUrl }: GmailColorProbeProps) {
return (
<Html lang="en">
<Head />
<Preview>Check the reset button and the text below it.</Preview>
<Body style={{ margin: 0, backgroundColor: "#f3f4f6" }}>
<Container style={{
maxWidth: "560px", padding: "24px",
backgroundColor: "#ffffff", fontFamily: "Arial, sans-serif",
}}>
<Text style={{ color: "#111827", fontSize: "24px" }}>
Reset your password
</Text>
<Text style={{ color: "#374151", fontSize: "16px" }}>
You requested a password reset. Use the button to continue.
</Text>
<Button href={actionUrl} style={{
backgroundColor: "#1e40af", color: "#ffffff",
padding: "14px 20px", fontSize: "16px",
}}>
Choose a new password
</Button>
<Text style={{ color: "#374151", fontSize: "14px" }}>
If you did not request this, you can ignore this email.
</Text>
</Container>
</Body>
</Html>
);
}
GmailColorProbe.PreviewProps = {
actionUrl: "https://example.com/reset-test",
} satisfies GmailColorProbeProps;Send this fixture through the same rendering and delivery path as the original email. Keep the received message as your baseline. Add the logo next, then the original button styling, then any decorative containers. When the failure returns, you have a small enough example to reason about.
If the reduced email still fails, save its generated HTML before adding a workaround. That file is useful when reporting a client issue and when checking the next template revision. Preserve the original file alongside the candidate fix; replacing it loses the evidence that explains why the workaround exists.
Give the logo contrast inside the asset
A black wordmark on a transparent PNG depends on whatever sits behind it. If the app darkens that surface, the letters can disappear even though the image loaded correctly. CSS padding around the image does not solve that dependency if the padded container changes color too.
One practical option is a PNG with the wordmark and a small contrasting background exported together. Leave breathing room inside the image. That background belongs to the asset instead of a CSS declaration. Check the resulting badge in both themes: a large white rectangle can be readable and still look awkward.
- The wordmark carries its own contrasting surface.
- Only one image needs to load; no media-query swap is required.
- The background remains part of the design in both themes.
- Image blocking still requires useful alt text and a visible sender name.
- You still need to inspect the delivered result in the target apps.
If you keep separate light and dark assets for supporting clients, make the default asset readable on its own. Test with the style block removed. The email should still identify the sender without showing two competing logos. Reserve image swaps for enhancement; they should not be the only thing keeping the brand name visible.
Keep the instructions and action label as live text. Turning the whole email into a screenshot avoids some CSS decisions but makes image blocking, text resizing, and accessibility much harder to handle.
Check button text and background as a pair
Set the label color on the actual link, along with its background. A color inherited from a surrounding section is harder to inspect and easier to lose when the markup changes. Look at the rendered anchor, including any child spans, rather than assuming the component props describe the final HTML exactly.
Measure the starting palette before blaming the inbox. The WCAG contrast guidance requires at least 4.5:1 for normal text and 3:1 for qualifying large text at Level AA. A small button label usually belongs in the normal-text category. These are thresholds for the actual foreground and background pair, not a guarantee about how an email app will transform those colors.
When inspecting a screenshot, do not sample the softened edge of a letter and treat that pixel as the declared text color. Anti-aliasing blends edge pixels with the background. Use known rendered colors where available, and treat screenshot measurements as a diagnostic aid. Text over a photograph or gradient needs separate attention because the background behind each character is not constant.
This utility checks opaque six-digit sRGB colors. Run it against your design tokens and any color pairs you can reliably inspect after rendering. It intentionally rejects shorthand, alpha, and named colors rather than guessing what surface sits underneath them.
function luminance(hex: string): number {
if (!/^#[0-9a-f]{6}$/i.test(hex)) {
throw new Error("Expected an opaque six-digit hex color");
}
const channels = [1, 3, 5].map((offset) => {
const value = parseInt(hex.slice(offset, offset + 2), 16) / 255;
return value <= 0.04045
? value / 12.92
: ((value + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * channels[0]
+ 0.7152 * channels[1]
+ 0.0722 * channels[2];
}
export function contrastRatio(foreground: string, background: string): number {
const a = luminance(foreground);
const b = luminance(background);
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
}
const ratio = contrastRatio("#ffffff", "#1e40af");
if (ratio < 4.5) {
throw new Error("Button text does not meet the normal-text AA threshold");
}
console.log(ratio.toFixed(2));Do not round a ratio upward before deciding whether it passes. Keep the calculation precise and round only for display. Also check the account address, expiry text, support link, and footer. Those often use muted colors even when the heading has plenty of contrast.
If only the brand-colored button fails, try a simpler solid pair in the reproduction before introducing a client selector. This is a diagnosis step, not a claim that blue, black, or near-white is immune to inversion. The final decision comes from the received message.
When a Gmail-specific workaround is worth keeping
Rémi Parmentier's Gmail blend-mode investigation documents a targeted technique for white text affected by Gmail iOS color changes. It combines blend modes with a preserved background and a Gmail-specific selector. The original research dates to 2021. Read its assumptions before adapting it to a current template.
This is a candidate repair for a reproduced failure, not a default wrapper for every email. Start with one affected region. Document the exact client where it helps, the markup it depends on, and the fallback when the special rules are absent. A workaround without that record becomes unexplained CSS that nobody wants to remove.
Reject the patch if it repairs one dark screenshot but damages the light version or hides text in another required client. You may need to simplify the visual treatment. A reset email can tolerate a different button shade; it cannot tolerate an unreadable action label.
What to save before you call the bug fixed
- The original and repaired HTML use the same fixture data.
- Each screenshot names the app, OS, theme, and template revision.
- The logo, action label, body copy, and footer are readable in both themes.
- The email still makes sense with images blocked.
- Every link reaches the intended staging destination.
- Any client-specific CSS has a comment explaining the observed failure.
Keep these results with the template change. A future logo replacement can reintroduce the same problem without touching the CSS. Repeat the relevant checks when assets, colors, or rendering dependencies change; use the email visual regression guide for the wider release workflow.
- Reproduce the received email in the exact app that fails.
- Fix the default logo and color pairs before adding special selectors.
- Keep light-mode results beside dark-mode results when reviewing a repair.