Skip to content

Emails with hardcoded text colors can become unreadable in Lime CRM #4065

@Kiarokh

Description

@Kiarokh

The problem

When Lime CRM renders an email, the text color is whatever the sender chose when they wrote it — and the background comes from Lime CRM's own theme. Two pieces, two different sources. When they don't match, the result can be unreadable.

Image Image

Why this happens? For example, a sender wrote their email in a dark-mode email client. Or for some reason their email had a white text, and a dark background. You're using Lime CRM in light mode. → White text on a light background. Invisible. Or the opposite of above.

Handling this becomes even more complicated when the CRM user is "forcing" a dark or light mode (per individual user preference), but the email's content is carrying a <meta name="color-scheme"> tag, or prefers-color-scheme media queries inside the email's CSS. These would conflict with the forced scheme, while the email is inheriting from the browser or OS.

This affects two places where email content shows up:

  1. The activities feed — short snippets of incoming emails are mixed with internal comments via <limel-markdown>.
  2. The email viewer — the full original .eml file is shown via <limel-email-viewer>, inside the file viewer.

Both need to be addressed, but the right fix is different in each.


A bit of history

Back in March 2024 we got a feature request — Lundalogik/limepkg-email#590 — saying senders should be able to use color in their emails and have it show up in the feed. The classic example: someone writes "see my responses in red" and replies inside a previous email in red.

We addressed that by allowing inline color and background-color to pass through <limel-markdown>'s sanitizer. It worked. Senders' colors started showing up.

But it created the readability problem above. And — more importantly — it pushed an email-shaped responsibility onto a generic component. <limel-markdown> is used all over Lime CRM and Lime Elements, not just for email snippets. Asking a generic Markdown/HTML renderer to also "remember sender colors so emails look right" is the kind of responsibility creep that's hard to spot up front but expensive over time.


What we propose to do

Part 1 — <limel-markdown>: stop preserving inline colors

Remove color and background-color from the inline-style allowlist. The component goes back to rendering structure and inheriting its text color from the surrounding theme. That makes feed snippets readable in any theme — no more invisible text, and it makes the component's job simpler again.

But what about the "see my responses in red" use case from #590?

That use case still matters, just not at this layer. Now that the new <limel-email-viewer> exists, the original email is always one click away. I propose handling the rare case where a sender uses color for emphasis as a promoted action on the feed item itself ("View original email"), inside limeb-feed (the foundation of the activity feed in lime-crm-building-blocks).

Image 💡 We can also display the promoted action for a better discoverability 👇 Image

That's where edge-case fidelity belongs — it's a product-level affordance, not a rendering concern. Most feed items don't need to reproduce sender colors, and the few that do are best handled by opening the original.

Part 2 — <limel-email-viewer>: theme-following surface, with a one-click escape

The email viewer sets a sensible default background based on the app theme:

  • App in light mode → email body has a light surface (off-white, with a gentle bluish-grey tone).
  • App in dark mode → email body has a dark surface (off-black, with a gentle bluish tone).

Note

The chosen tones aren't pure white or pure black — they have a faint hue. That means even when an email's hardcoded text color is exactly #fff or #000, there's still a sliver of contrast. There's never a complete white-on-white or black-on-black wash-out.

For the cases that still will happen: (e.g. a dark-mode-composed email viewed in light mode), there's a small button labeled "Hard to read?" sitting at the top of the email body. One click flips the email's background to the opposite tone. The email content itself is not modified — only the surface behind it changes.

A tooltip on the button explains the why in plain language, so users encountering the button for the first time understand what it does.

Screen.Recording.2026-05-07.at.20.10.04.mov

That gives the user a clean escape for any combination of (app theme × email text color), with one click, and without us touching the sender's bytes.

💡 In future, we can extend this functionality and make it a "rememberable" thing per email. And save it in the database.

Approaches we considered and rejected

Strip inline colors from the email viewer too

The same trick we used for <limel-markdown>: just delete inline color and background-color declarations from email HTML. That works for readability — but it destroys the very thing the email viewer is for. The whole point of the email viewer is fidelity — letting the user see what the sender actually sent. Stripping declarations would break that promise.

Auto-detect the email's "intended" color scheme

Some emails carry hints about their intended display mode (a <meta name="color-scheme"> tag, or prefers-color-scheme media queries inside the email's CSS). We could parse those and pick a surface accordingly. But:

  • Most emails include no hint at all.
  • When a hint is present, it usually means "I support both light and dark" rather than "I am dark." Acting on that as if it were certainty produces wrong guesses.
  • Even if we did detect well, we'd still need a manual override for the cases we got wrong — so we'd be paying complexity and still need the manual button. As explain earlier, things will get complicated when user forces dar/light mode, but browser or OS are saying something else, and we have custom CSS vars for colors which are automatically switching according to user's scheme preference in the app.

Walk the rendered DOM and adjust low-contrast colors per element

This was actually attempted earlier in PR #4060. The idea: after the email renders, walk every text element, measure its color's contrast against the resolved background, and strip declarations below a 3:1 threshold. Save the original color on a data- attribute so it can come back when the theme flips.

It works. But it brings a lot with it:

  • Walking the entire rendered DOM on every email render.
  • A WCAG contrast calculation, plus a parser for every CSS color format senders might use.
  • A MutationObserver and a media-query listener to react to theme changes at runtime.
  • Per-element bookkeeping so stripped colors can be restored.
  • Long term maintenance and edge cases that we don't see

When that approach was reproposed as a <limebb-markdown> wrapper inside lime-crm-building-blocks (so the generic <limel-markdown> could stay simple), the cost didn't change — it just relocated. The complexity is the behaviour, not the address. And it still manipulates the email's content, even if the manipulation is reversible.

For a problem the user encounters occasionally, this is an expensive answer. The "Hard to read?" button does the same job for a fraction of the code, and it never touches the sender's content.


Summary

  • <limel-markdown> goes back to being a clean, theme-neutral renderer. Inline colors no longer survive sanitization, so feed snippets are always readable.
  • The "preserve sender emphasis colors in the feed" need from limepkg-email#590 is best handled at the product level — a "See the original email" promoted action on the feed item — not at the rendering layer.
  • <limel-email-viewer> adapts its background to the app theme, and offers a small "Hard to read?" button for the rare cases where the email's hardcoded colors clash with the surface. The email content is never modified.
  • We deliberately chose not to detect, calculate, or auto-adapt anything. The cost outweighs the benefit, and a single-click manual override solves the problem with almost no machinery.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions