Skip to content

fix(viewer): restore sender colours in dark mode - #949

Open
shukiv wants to merge 1 commit into
bulwarkmail:mainfrom
shukiv:fix/dark-mode-sender-colours
Open

fix(viewer): restore sender colours in dark mode#949
shukiv wants to merge 1 commit into
bulwarkmail:mainfrom
shukiv:fix/dark-mode-sender-colours

Conversation

@shukiv

@shukiv shukiv commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Problem

In dark mode, ordinary HTML mail comes out with the sender's colours wrong: a navy header band renders pale blue, a white message body renders black. Reported against a standard transactional template.

Cause

The dark-mode CSS inverts the body and then re-inverts colour containers so they return to what the sender designed. Two rules stopped that second step from happening.

1. body was not excluded from the nested-container rule.

:where([style*="background:"], [bgcolor])
  :where([style*="background:"], [bgcolor]):not(:has(…)) { filter: none !important; }

A mail whose own body carries a background — <body style="margin:0;padding:0;background:#fff;">, which is standard in transactional templates — makes every colour container in the message a descendant of a colour container. All of them get filter: none, nothing is re-inverted, and the entire email stays inverted. That is the white-body-renders-black half.

2. Colour containers wrapping media were skipped entirely.

The :not(:has(img, video, …)) guard means a header band containing a logo never gets re-inverted, so the band keeps the body's inversion. That is the navy-renders-pale-blue half.

Change

Exclude body from being an ancestor in the nested rule (and from the re-invert itself — it is already inverted by the rule above, so it must not flip back).

Re-invert colour containers even when they wrap media, and cancel the per-image rule inside them — exactly what the background-image branch immediately below already does. The image ends up with the same net treatment as before, while the container's colour is restored.

That cancel needs a real attribute selector rather than :where(): :where() contributes no specificity, so a :where(…) :where(img) form loses to the bare img { filter: … } rule and silently does nothing.

Verification

Reproduced the exact markup from the reported message in a browser and read computed styles, before and after:

element before after
header band (background: + logo) none invert(1) hue-rotate(180deg)
body table (background:#ffffff) none invert(1) hue-rotate(180deg)
logo <img> double transform single, via the container

Rendering matches: navy band, white body, both restored.

tsc --noEmit clean; components/email passes (129 tests). Running on a production instance.

Not addressed

Colours still do not round-trip perfectly through two invert() hue-rotate() passes — hue-rotate is a linear approximation, so a saturated yellow logo comes back slightly warm. That is inherent to the technique rather than to these selectors, and this change does not make it worse: the image goes through two transforms either way.

Dark mode inverts the message body and then re-inverts colour containers
so they come back to what the sender designed. Two rules stopped that
second step happening on ordinary mail.

The nested-container rule treats any colour container inside another as
already correct. It never excluded body, and a mail whose own body
carries a background — `body style="background:#fff"`, which is standard
in transactional templates — therefore made every container in the
message count as nested. Nothing was re-inverted and the whole email
stayed inverted: white bodies rendering black.

Colour containers wrapping media were skipped outright, which left the
container's own colour inverted. A navy header band with a logo in it
rendered pale blue. Re-invert it and cancel the per-image rule inside
instead, which is what the background-image branch alongside it already
does, and gives the image the same net treatment while restoring the
band. The cancel needs a real attribute selector, not :where(), or the
bare `img` rule outweighs it.

Confirmed against a real message in a browser, reading computed styles
before and after: the header band and body both went from filter "none"
to being re-inverted, and the logo from a double transform to one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant