Skip to content

feature(dark-mode) added dark mode support#3068

Open
dazgreer wants to merge 11 commits intofix/replace-html-minifierfrom
feature/dark-mode-support
Open

feature(dark-mode) added dark mode support#3068
dazgreer wants to merge 11 commits intofix/replace-html-minifierfrom
feature/dark-mode-support

Conversation

@dazgreer
Copy link
Copy Markdown
Collaborator

@dazgreer dazgreer commented Apr 9, 2026

Summary

Overhauls MJML dark-mode support across body components giving the user the option to support and attribute tools to make simple changes to colours and images in clients that support it.

What Changed

  1. New shared dark-mode infrastructure in core

    • Added shared dark-mode rule registration/emission helpers.
    • Centralised dark CSS generation under a single grouped prefers-color-scheme: dark style emission path.
    • Added Outlook dark-mode helper utilities for dark image/background handling and shared style emission.
    • Extended global render state to track dark-mode rule/style emission and avoid duplicate style blocks.
  2. Component dark-mode support expanded and normalised
    Implemented/extended dark attributes and rendering behaviour for:

    • mj-accordion (and children) - color / border color / container background color / icon images
    • mj-body - background color
    • mj-button - color / border color / background color / container background color
    • mj-carousel (and children) - border color(s) / container background color / icon images / images
    • mj-column - border color(s) / background color (inner and outer)
    • mj-divider - border color / container background color
    • mj-group - background color
    • mj-hero - background color (inner and outer) / background image
    • mj-image - border color(s) / container background color / image
    • mj-navbar (and children) - icon color / color
    • mj-section - border color / background color / background image
    • mj-social (and children) - color / background color / container background color / image
    • mj-spacer - container background color
    • mj-table - color / border color / container background color
    • mj-text - color / container background color
    • mj-wrapper (inherits section behaviour) - border color / background color / background image
  3. Addiitioal ‘Outlook’ support for images in various (not all) Outlook clients

    • mj-carousel
    • mj-image
    • mj-social
  4. Validator rule for dark attribute usage

    • Added validator rule that warns when dark-mode attributes are used without root support-dark-mode="true".
  5. Added dark-mode test coverage across components, including:

    • correct class placement,
    • dark rule coalescing,
    • single shared media-block emission in integration scenarios,
    • Outlook-specific dark-image style emission,
    • inheritance/override behaviour.
  6. Docs updates

    • Updated component READMEs to document newly supported dark-mode attributes and behaviour.

dazgreer added 8 commits March 5, 2026 15:30
- mj-title throws an error if missing or empty
- optional Outlook and dark mode support
- add space to preview text
- comprehensive tidy of HTML to reduce code bloat
- updated tests and docs
mjml-core
- Updated Prettier to use single quotes inside of CSS
- Broken out the accordion CSS into its own style block as it was breaking other CSS and causing non functional components, e.g. carousel in Gmail and updated test

mjml-section
- fixed issues caused by removing background-size and background-repeat as default attributes whereby the default values were used to determine VML settings. Created automated test
- removed multiple declarations of the background color and concatenated two divs that were split because of this
- updated table to use role=“none”
…d-tidy

- Merged cssnano-preset-lite improvements and normalizeMinifyCssOption helper from fix/replace-html-minifier
- Adopted Mocha-based *.test.js pattern for mjml-core tests (replacing old *-test.js runner)
- Preserved accordion-style and dark-mode skeleton tests from feature branch in skeleton.test.js
cheerio is a webpack external for the browser bundle, so calling
load() inside mergeHeadStyleBlocks() crashed the smoke test with
'Cannot read properties of undefined (reading load)'.

Replace the cheerio DOM walk with a plain character scanner that
tokenises <head> content into plain-style / whitespace / other
segments and merges consecutive eligible <style> blocks inline.
The merged output is identical; the import of load from cheerio
is retained for the mj-html-attributes feature at the call-site
that is already correctly guarded by an isEmpty() check.

Also fixes no-continue lint errors by using an 'advanced' flag
instead of continue statements in the tokenizer loop.
…t tests

- extracted mergeHeadStyleBlocks into its own helper module and imported
- added test file covering 29 unit tests.
- added support-dark-mode switch to include relevant meta and CSS
- added dark- prefixed classes to aid with dark mode changes for colors and images
- additional support for image changes in various Outlook clients
- added validation for new attributes
- added automated testing and updated documentation
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Expands and standardizes MJML dark-mode support across core rendering and multiple body components, including shared CSS rule emission, optional Outlook-specific dark image handling, and new validator warnings for missing root dark-mode opt-in and missing/empty mj-title.

Changes:

  • Added shared dark-mode infrastructure in mjml-core (rule registration, single prefers-color-scheme: dark emission path, head/style block merging, Outlook utilities).
  • Implemented/normalized dark-* attributes across many components (text/table/spacer/social/navbar/group/divider/body/accordion/wrapper, plus docs and smoke/integration tests).
  • Added validator rules + tests for missing/empty mj-title and using dark-* attrs without support-dark-mode="true" on <mjml>.

Reviewed changes

Copilot reviewed 89 out of 89 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/mjml/test/wrapper-dark-smoke.test.js Wrapper dark-mode smoke coverage (bg color/image + coalescing).
packages/mjml/test/validator-title.test.js Tests for new mj-title validation warnings.
packages/mjml/test/utils.js Safer extractStyle helper for tests.
packages/mjml/test/text-dark-color.test.js mj-text dark color/container bg tests + rule grouping assertions.
packages/mjml/test/tableWidth.test.js Adjusts width assertions (handles auto => omitted).
packages/mjml/test/table-dark-color-border-container-background-color.test.js mj-table dark color/border/container bg tests.
packages/mjml/test/spacer-dark-container-background-color.test.js mj-spacer dark container bg tests.
packages/mjml/test/social-dark-src-head-style.test.js Outlook dark-src head-style behavior tests for mj-social-element.
packages/mjml/test/social-dark-color-background-color.test.js mj-social / mj-social-element dark color/bg inheritance tests.
packages/mjml/test/section-dark-background-url.test.js mj-section dark background-url tests.
packages/mjml/test/section-background-url-no-background-size.test.js Regression test: section bg-url without bg-size + beautify quoting.
packages/mjml/test/navbar-dark-colors.test.js mj-navbar/link dark color and class placement tests.
packages/mjml/test/image-dark-src-head-style.test.js Outlook dark-src head-style emission tests for mj-image.
packages/mjml/test/image-dark-border-container-background-color.test.js mj-image dark border/container bg behavior tests.
packages/mjml/test/hero-dark-background-url.test.js mj-hero dark background-url tests.
packages/mjml/test/hero-dark-background-color.test.js mj-hero dark bg + dark inner bg tests and class placement.
packages/mjml/test/group-dark-background-color.test.js mj-group dark background-color tests.
packages/mjml/test/divider-dark-border-container-color.test.js mj-divider dark border/container bg tests.
packages/mjml/test/carousel-dark-src.test.js mj-carousel dark sources for images/thumbnails/icons tests.
packages/mjml/test/carousel-dark-colors.test.js mj-carousel dark container/bg + thumbnail border override tests.
packages/mjml/test/button-dark-color-background-border-container-background-color.test.js mj-button dark color/bg/border/container bg tests.
packages/mjml/test/body-dark-background-color.test.js mj-body dark background + coalesced rule emission tests.
packages/mjml/test/accordion-dark-colors.test.js mj-accordion* dark color/background/border/icon tests.
packages/mjml-wrapper/src/index.js Refactors MSO wrappers using shared msoConditionalTag.
packages/mjml-wrapper/README.md Documents wrapper dark-* attributes.
packages/mjml-validator/src/rules/validTag.js Allows mjml as a validator-permitted tag.
packages/mjml-validator/src/rules/requireSupportDarkModeForDarkSrc.js New validator rule warning when dark-* used without root opt-in.
packages/mjml-validator/src/rules/requiredTitle.js New validator rule warning for missing/empty mj-title.
packages/mjml-validator/src/MJMLRulesCollection.js Registers new validator rules.
packages/mjml-validator/src/index.js Enables validation traversal for <mjml> (no skip list).
packages/mjml-text/src/index.js Adds dark-color / dark-container-background-color support + shared head style emission.
packages/mjml-text/README.md Documents mj-text dark-mode attributes and note.
packages/mjml-table/src/index.js Adds dark-* support; tweaks width/style emission.
packages/mjml-table/README.md Documents mj-table dark-mode attributes and note.
packages/mjml-spacer/src/index.js Adds dark-container-background-color with shared dark-mode CSS emission.
packages/mjml-spacer/README.md Documents mj-spacer dark-mode attribute and note.
packages/mjml-social/src/Social.js Adds dark container/color support; refactors MSO conditionals usage.
packages/mjml-social/README.md Documents dark-mode attributes and Outlook dark-image support options.
packages/mjml-section/README.md Documents section dark-mode attributes and note.
packages/mjml-parser-xml/test/preprocessors.test.js Updates parser test fixtures to include required mj-title.
packages/mjml-navbar/src/NavbarLink.js Adds dark-color support + shared dark-mode head style emission.
packages/mjml-navbar/README.md Documents navbar/link dark-mode attributes and note.
packages/mjml-image/README.md Documents image dark-* attrs + Outlook dark image option and note.
packages/mjml-hero/README.md Documents hero dark-* attributes and note.
packages/mjml-head-title/README.md Documents new validator behavior for missing/empty mj-title.
packages/mjml-head-preview/src/index.js Adds fill-space preview padding behavior.
packages/mjml-head-preview/README.md Documents mj-preview new attributes.
packages/mjml-head-attributes/README.md Fixes typo (“within”).
packages/mjml-group/src/index.js Adds dark-background-color support + Outlook conditional refactor.
packages/mjml-group/README.md Documents group dark background attribute and note.
packages/mjml-divider/src/index.js Adds dark-* support and refactors divider rendering/outlook handling.
packages/mjml-divider/README.md Documents divider dark-mode attributes and note.
packages/mjml-core/tests/skeleton.test.js Extends skeleton tests for accordion style block + dark-mode meta tags.
packages/mjml-core/src/index.js Tracks new global dark-mode state; head-style merging; output formatting changes.
packages/mjml-core/src/helpers/styles.js Separates accordion head CSS into its own <style type="text/css">.
packages/mjml-core/src/helpers/skeleton.js Adds opt-in dark-mode meta/CSS; restructures head markup and namespaces.
packages/mjml-core/src/helpers/outlookDarkMode.js New Outlook dark-mode image/background rule registry + head emission.
packages/mjml-core/src/helpers/mergeOutlookConditionnals.js Improves conditional merging logic to avoid negation edge cases.
packages/mjml-core/src/helpers/mergeHeadStyleBlocks.js New helper to coalesce consecutive plain <style> blocks in <head>.
packages/mjml-core/src/helpers/mediaQueries.js Adjusts media query <style> generation and optional OWA desktop forcing.
packages/mjml-core/src/helpers/fonts.js Changes font import emission to use conditional-tag wrapper.
packages/mjml-core/src/helpers/conditionalTag.js Updates MSO conditional formats + adds global Outlook-classic enable/disable flag.
packages/mjml-core/src/helpers/colorSchemeDarkMode.js New shared dark-mode rule registry + single head <style> emission.
packages/mjml-core/src/createComponent.js Avoids emitting empty class/style attributes; adds boolean attribute handling.
doc/components_1.md Updates docs example + documents new <mjml> root options.
packages/mjml-column/README.md Documents column dark-mode attributes and note.
packages/mjml-carousel/README.md Documents carousel dark-mode attributes and Outlook image support note.
packages/mjml-button/README.md Documents button dark-mode attributes and note.
packages/mjml-body/src/index.js Adds dark-background-color support and shared dark-mode head style emission.
packages/mjml-body/README.md Documents body dark-background-color and note.
packages/mjml-accordion/src/AccordionTitle.js Adds dark bg/color + dark icon URL support; shared rule grouping.
packages/mjml-accordion/src/AccordionText.js Adds dark bg/color + inherited border dark-mode support.
packages/mjml-accordion/src/AccordionElement.js Adds dark background/border + dark icon attribute plumbing.
packages/mjml-accordion/src/Accordion.js Adds accordion dark container/border support + shared head style emission.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +113 to +117
if (supportOutlookClassic) {
return `
<table
${this.htmlAttributes({
align: this.getAttribute('align'),
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

render() currently switches to emitting a <table> for all clients whenever supportOutlookClassic is true. That flag should only control whether Outlook-specific conditional markup is included, not change the base markup for non-Outlook clients; otherwise this is a breaking output change. Consider always rendering the normal <p> and wrapping the Outlook-only <table> in an MSO conditional (or using the existing conditionalTag helper), gated by supportOutlookClassic.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is intentional behaviour, the component uses either table or p based on support-outlook-classic and only outputs one.

Comment on lines 22 to +23
static defaultAttributes = {
direction: 'ltr',
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

direction is allowed and used in getStyles() but defaultAttributes is now empty, so direction becomes undefined by default (regression vs previous ltr default). Add direction: 'ltr' back (and any other removed defaults) to preserve existing rendering.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Intentionally removed as a default because ltr is the client default. getStyles() using this.getAttribute('direction') with no value set will return undefined, and MJML's CSS emission skips undefined values so the property is omitted

Comment on lines +533 to +538
if (node.tagName === 'mj-section' || node.tagName === 'mj-hero') {
const attrs = node.attributes || {}
const bg = attrs['background-url']
if (typeof bg === 'string' ? bg.trim().length > 0 : Boolean(bg)) {
return true
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

usesVML only checks mj-section and mj-hero for background-url, but mj-wrapper (and potentially other section-like components) also emit VML for background images. If a wrapper has background-url, usesVML stays false and skeleton() will omit the xmlns:v namespace, producing invalid VML output. Extend the scan to include all tags that can emit VML backgrounds (at least mj-wrapper).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Now scans mj-wrapper when deciding whether to include the xmlns:v namespace. This has been fixed to the PR here: #3059 and will be merged into this PR once approved.

supportDarkMode:
String(get(mjml, 'attributes.support-dark-mode', false)).toLowerCase() ===
'true',
supportOutlookClassic: get(mjml, 'attributes.support-outlook-classic', true) !== false,
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

supportOutlookClassic parsing won't work with MJML's string attributes: support-outlook-classic="false" arrives as the string 'false', but the current check ... !== false will still evaluate to true. Coerce like String(attr).toLowerCase() === 'true' (or the inverse) so users can reliably disable Outlook-classic output.

Suggested change
supportOutlookClassic: get(mjml, 'attributes.support-outlook-classic', true) !== false,
supportOutlookClassic:
String(get(mjml, 'attributes.support-outlook-classic', true)).toLowerCase() !==
'false',

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Now changes the support-outlook-classic="false" attribute to a string before comparing, correctly evaluating to false. This has been fixed to the PR here: #3059 and will be merged into this PR once approved.

…ibutes

- background-color applied to body tag in mj-body
- background-color applied to parent table tag instead of both td tags in mj-accordion-title
@dazgreer dazgreer changed the title Feature/ added dark mode support feature(dark-mode) added dark mode support Apr 9, 2026
dazgreer added 2 commits April 9, 2026 15:50
- now scans mj-wrapper when deciding whether to include the xmlns:v namespace.
- now changes the support-outlook-classic="false" attribute to a string before comparing, correctly evaluating to false. Added automated test
…upport

# Conflicts:
#	packages/mjml-body/src/index.js
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.

2 participants