Skip to content

feat(ui): preview the dark styles an email defines - #3712

Closed
creativoma wants to merge 8 commits into
resend:canaryfrom
creativoma:fix/preview-native-dark-mode
Closed

creativoma wants to merge 8 commits into
resend:canaryfrom
creativoma:fix/preview-native-dark-mode

Conversation

@creativoma

@creativoma creativoma commented Aug 18, 2026 •

Copy link
Copy Markdown

Closes #3707. Implements the proposal in #3609.

Problem

The preview's dark mode toggle emulates forced color inversion — what Gmail and Outlook.com do to a light theme. Clients that honor prefers-color-scheme, such as Apple Mail and Outlook for Mac, instead render whatever dark theme the email authored, and that behavior had no representation in the preview.

Because the inversion walks the DOM recoloring inline styles, it never evaluates a media query. An email shipping real @media (prefers-color-scheme: dark) rules — which includes anything using Tailwind's dark: variants — could not be previewed at all.

Changes

The single toggle becomes two, so both client behaviors are reachable and each is labelled for what it does:

Control Emulates
Emulated color inversion Clients that ignore prefers-color-scheme (Gmail, Outlook.com)
Dark styles defined by the email Clients that honor it (Apple Mail, Outlook for Mac)

The existing ?dark search param keeps meaning color inversion, so links written before this change resolve the same way.

How the scheme is applied

Setting color-scheme on the frame is enough wherever the browser propagates it into the embedded document, and there the email's own rules match untouched. That propagation is Firefox 105+ and Chromium 129+ only; Safari does not ship it yet (it landed in Safari Technology Preview 242). Left at that, an email would render one way on Chrome and another on Safari.

So where the browser does not do it, forceColorScheme flips the value of every prefers-color-scheme condition, turning the queries the reader's real setting matches into the ones it doesn't. Only the leaf value changes, so the browser still evaluates the surrounding and/or/not and the comma-separated query list. Rules keep their position in the cascade, and prefers-color-scheme: light blocks are handled alongside the dark ones. Only the live CSSOM is touched, never the <style> elements' text, so the source view still shows the email as authored.

Fixes along the way

  • With dark mode off, an email's dark rules matched whenever the reader's own OS was set to dark, and the inversion emulation then recolored an already-dark theme. Both are now decided by the preview rather than by the machine it runs on.
  • Undoing the inversion left a white background on an email that never painted one. The inversion gives <body> the colors it already renders at so it has something to invert; those were being written back as though the email had authored them.

Testing

36 unit tests covering the condition rewriting, the URL state, and syncDarkMode against a real frame.

Verified end to end in Chromium 147, Chrome 151 and WebKit 26.4, across all three modes with the OS set to light and to dark — including the non-propagating path that Safari takes today.

Out of scope

Color inversion still only recolors inline styles; rules in a <style> block are not inverted. That is unchanged behavior and separate from what this PR addresses.


Summary by cubic

Adds a native dark-mode preview alongside the existing inversion. The preview now renders an email’s authored dark styles independently of the reader’s OS; ?dark still selects inversion, while ?dark=native selects native mode.

New Features

  • Replaces the single toggle with labeled controls for emulated inversion and email-defined dark styles.
  • Rewrites prefers-color-scheme rules when embedded browsers do not propagate color-scheme, without changing the email source.
  • Keeps the preview backdrop consistent across dark modes.

Bug Fixes

  • Prevents dark mode from inheriting the reader’s OS setting when disabled or using inversion.
  • Removes temporary body colors when inversion ends while restoring colors authored by the email.
  • Adds coverage for URL state, media-query rewriting, and frame synchronization.

Written for commit 3dead1f. Summary will update on new commits.

Review in cubic

`tsc --build` writes a .tsbuildinfo next to the tsconfig that produced it,
which was showing up as an untracked file under apps/web.
The preview's dark mode toggle emulated forced color inversion, the way
clients that ignore `prefers-color-scheme` recolor a light theme. Clients
that honor it instead render whatever dark theme the email authored, and
that half had no representation: an email shipping real
`@media (prefers-color-scheme: dark)` rules could not be previewed at all.

Split the control in two so both behaviors are reachable, and add
`forceColorScheme`, which decides which of the email's color scheme rules
apply rather than leaving it to the machine the preview runs on. Setting
`color-scheme` on the frame covers this wherever the browser propagates it
into the embedded document, but that is Firefox 105+ and Chromium 129+ only
— Safari does not ship it yet. Where it is missing, every
`prefers-color-scheme` condition has its value flipped, turning the queries
the reader's setting matches into the ones it doesn't. Only the leaf value
changes, so the browser still evaluates the surrounding operators and the
query list.

This also settles a case the toggle got wrong before: with dark mode off, an
email's dark rules matched whenever the reader's own OS was set to dark, and
the inversion emulation then recolored an already-dark theme.

Only the live CSSOM is touched, never the `<style>` elements' text, so the
markup shown in the source view stays the email as authored.
The color inversion only recolors values it finds inline, so it gives
`<body>` the colors it already renders at to have something to invert. Those
were recorded alongside the ones the email really declared, and so were
written back when the inversion was undone, leaving an email that never
painted a background of its own with a white one for as long as the frame
lived.

It went unnoticed while the toggle had two states, since turning dark mode
off meant a white page anyway. Under the mode that renders an email's own
dark theme it shows as a white page behind a dark email.

Mark the properties the inversion adds so undoing removes them, and keep
restoring the ones the email authored.
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@creativoma is attempting to deploy a commit to the resend Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3dead1f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@react-email/ui Minor
react-email Minor
@react-email/editor Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
react-email Ready Ready Preview Sep 22, 2026 8:06pm UTC
react-email-demo Ready Ready Preview Sep 22, 2026 8:06pm UTC

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Sep 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@react-email/ui@3712

commit: 1dbdb35


// Email clients are split on what they do with a dark-mode-aware email, so each
// behavior gets its own button rather than one toggle guessing between them.
const modes = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should think about 3 options here:

  • light (inversion as before)
  • dark (inversion as before)
  • and new the native option.

Its very confusion to understand with just two options, since the first option does two actions (inverts btween light/dark)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it's confusing right now! I see it like this: we always have the dark/light, but we can toggle on/off prefers-color-scheme. And maybe the header is not the best place for this? I think we should take a closer/deeper look first before merging!

'@react-email/ui': minor
---

Split the preview's dark mode control in two, so both of the behaviors email clients have can be previewed: emulated color inversion, as clients that ignore `prefers-color-scheme` apply it, and the dark styles the email itself defines, as clients that honor it render them. The second is new — until now the toggle could only ever invert the light theme, so an email shipping real `@media (prefers-color-scheme: dark)` rules had no way to be previewed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't really need to explain in changelog what is the whole implementation is doing. We prefer to keep it short, focusing on what changed/whats news for quick reading the changelog and identify if something can help you and worth updgrating or something is related to a bug/issue

Comment on lines +111 to +113
// What the `<Tailwind>` component emits, and what happy-dom preserves
// verbatim — so matching on `prefers-color-scheme: dark` with a literal
// space would miss it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove all these comments within tests?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overal there are too many comments in these files, making it hard to understand the code and implementation itslf. can u please clean up?

@dielduarte

Copy link
Copy Markdown
Contributor

Closing this PR for now; see https://github.com/resend/react-email/discussions/3609#discussioncomment-18567783.lets discuss what to do and then either reuse this PR or open a new one.

@dielduarte dielduarte closed this Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

linear-synced PR has been synced to Linear

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Preview dark mode toggle only inverts inline colors, never renders actual prefers-color-scheme styles

3 participants