Skip to content

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

Description

@MILLERMARRU

Description

I read through the preview's dark mode toggle implementation (packages/ui/src/app/preview/[...slug]/email-frame.tsx) after seeing #3609, and the toggle can't render an email's actual @media (prefers-color-scheme: dark) styles at all, only simulate one.

applyColorInversion/undoColorInversion walk the iframe DOM and invert inline style colors element by element:

function applyColorInversion(iframe: HTMLIFrameElement) {
  // ...
  for (const element of walkDom(contentDocument.documentElement)) {
    // ...
    for (const [properties, type] of styleProperties.entries()) {
      for (const property of properties) {
        const value = element.style[property]
        if (value && value.trim() !== '') {
          element.setAttribute(`data-original-${property}`, value)
          element.style[property] = value
            .replaceAll(colorRegex(), (color) => invertColor(color, type))
          // ...
        }
      }
    }
  }
}

It never touches <style> tags or evaluates any @media query. For any email that ships real prefers-color-scheme: dark rules in a <style> block, which is most dark-mode-aware markup, including anything using Tailwind's dark: classes, the toggle shows a color-inverted version of the light styles instead of the email's actual authored dark styles. There's no way in the preview UI today to see what the email genuinely looks like in a client that respects prefers-color-scheme (Apple Mail, etc).

Reproduction

  1. Author an email with <style>{'@media (prefers-color-scheme: dark) { .card { background: #111 } }'}</style> (or the Tailwind dark: equivalent).
  2. Open it in the preview and toggle dark mode.
  3. Compare against opening the same rendered HTML directly in a mail client (or a browser tab with OS dark mode forced) that actually evaluates prefers-color-scheme.

The preview shows an inverted light theme; the real dark-mode styles never apply.

Suggested fix

The iframe is already rendered with sandbox="allow-same-origin ..." and srcDoc content (same origin as the parent app), which applyColorInversion itself relies on to reach into contentDocument directly. The same access works for reading doc.styleSheets, collecting rules inside a CSSMediaRule whose media.mediaText includes prefers-color-scheme: dark, and injecting their unwrapped cssText into a single <style> appended to the iframe's <head> when the toggle is on (removing it when toggled off). That would render the email's actual authored dark theme instead of a simulated inversion, matching what Apple Mail / Outlook for Mac do.

Happy to put together a PR for this if it's a direction the team wants, I already have a rough implementation sketched out against email-frame.tsx.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions