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.
| if (supportOutlookClassic) { | ||
| return ` | ||
| <table | ||
| ${this.htmlAttributes({ | ||
| align: this.getAttribute('align'), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
This is intentional behaviour, the component uses either table or p based on support-outlook-classic and only outputs one.
| static defaultAttributes = { | ||
| direction: 'ltr', | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
packages/mjml-core/src/index.js
Outdated
| 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 | ||
| } |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
packages/mjml-core/src/index.js
Outdated
| supportDarkMode: | ||
| String(get(mjml, 'attributes.support-dark-mode', false)).toLowerCase() === | ||
| 'true', | ||
| supportOutlookClassic: get(mjml, 'attributes.support-outlook-classic', true) !== false, |
There was a problem hiding this comment.
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.
| supportOutlookClassic: get(mjml, 'attributes.support-outlook-classic', true) !== false, | |
| supportOutlookClassic: | |
| String(get(mjml, 'attributes.support-outlook-classic', true)).toLowerCase() !== | |
| 'false', |
There was a problem hiding this comment.
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
- 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
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