feature(dark-mode) added dark mode support#3068
feature(dark-mode) added dark mode support#3068dazgreer wants to merge 11 commits intofix/replace-html-minifierfrom
Conversation
- 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
There was a problem hiding this comment.
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, singleprefers-color-scheme: darkemission 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-titleand usingdark-*attrs withoutsupport-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.
…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
- 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
| | lang | string | adds a `lang` attribute in the `html` and `body > div` tags | `und` | | ||
| | support-dark-mode | boolean | setting to `true` will add `<meta>` tags and `:root` CSS to enable support | `false` | | ||
| | support-outlook-classic | boolean | setting to `false` will remove all Outlook specific code when compiled to HTML | `true` | | ||
| | owa | string | if set to `desktop`, this will force the desktop version for older (self-hosted) versions of Outlook.com that don't support media queries (cf. [this issue](https://github.com/mjmlio/mjml/issues/2241)) | `none` | |
There was a problem hiding this comment.
Instead of string, should the type be desktop | none? Do we have something like that in the doc already?
There was a problem hiding this comment.
Is this for the owa? That's not a part of these updates, it was already present in the previous version
| .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-content { overflow: hidden; display: block; } | ||
| .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-ico { display: none; } | ||
|
|
||
| /* prettier-ignore */ |
There was a problem hiding this comment.
Needed? The comment will stay in the HTML, no?
There was a problem hiding this comment.
This is part of the previous HTML changes PR but anyway, it is necessary otherwise Prettier breaks the Accordion in Gmail as it chokes on the line below.
All prettier comments are removed when compiled though here: https://github.com/mjmlio/mjml/pull/3059/changes#diff-17b00ecc0c06136aa56a592b0e9154976c812427d95ca6ce9c0fc3c4d2e05305R931
| border: this.getAttribute('border'), | ||
| 'border-bottom': 'none', | ||
| 'font-family': this.getAttribute('font-family'), | ||
| ...(this.getAttribute('border') !== 'none' && this.getAttribute('border') !== '0' && this.getAttribute('border') !== '0px' && { 'border-bottom': '0' }), |
There was a problem hiding this comment.
| ...(this.getAttribute('border') !== 'none' && this.getAttribute('border') !== '0' && this.getAttribute('border') !== '0px' && { 'border-bottom': '0' }), | |
| ...(!['none', '0', '0px'].includes(this.getAttribute('border')) && { 'border-bottom': '0' }), |
| width: '100%', | ||
| 'border-bottom': this.getAttribute('border'), | ||
| 'background-color': this.getAttribute('background-color'), | ||
| ...(this.getAttribute('border') !== 'none' && this.getAttribute('border') !== '0' && this.getAttribute('border') !== '0px' && { 'border-bottom': this.getAttribute('border') }), |
| 'xml:lang': lang, | ||
| })}> | ||
| ${buildPreview(preview)} | ||
| <div${this.htmlAttributes({ |
There was a problem hiding this comment.
I think we are missing spaces in a lot of places between the tag element and the call to ${this.htmlAttributes({..., no?
There was a problem hiding this comment.
Double spaces were compiling in the code
| constructor(initialDatas = {}) { | ||
| super(initialDatas) | ||
| this.carouselId = genRandomHexString(16) | ||
| this.carouselId = genRandomHexString(6) |
There was a problem hiding this comment.
Why do we moved from 16 to 6?
There was a problem hiding this comment.
This is also from the previous HTML PR. It's to save bloat. Each carousel writes out the ID 41 times so this saves 0.41kb per carousel. 6 was chosen as still fairly conservative (1 in 16 million) whilst making a significant saving in code
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
New shared dark-mode infrastructure in core
Component dark-mode support expanded and normalised
Implemented/extended dark attributes and rendering behaviour for:
Addiitioal ‘Outlook’ support for images in various (not all) Outlook clients
Validator rule for dark attribute usage
Added dark-mode test coverage across components, including:
Docs updates