Skip to content

Initiate Morpheus Dark mode#23912

Open
tzi wants to merge 29 commits into5.x-devfrom
dark-mode
Open

Initiate Morpheus Dark mode#23912
tzi wants to merge 29 commits into5.x-devfrom
dark-mode

Conversation

@tzi
Copy link
Copy Markdown
Contributor

@tzi tzi commented Dec 23, 2025

Description

This PR introduces dark mode for the main authenticated Matomo UI. ( UX-120 )

How to use it

  • Dark mode is available in the authenticated Morpheus-based UI.
  • Go to Personal > General Settings, use the Theme setting, choose Light (default) or Dark, then save.
  • The preference is stored per user, not globally for the whole Matomo instance.
  • The switch applies immediately in the current document by updating the root theme attribute after save.
  • Dark mode is not available during installation.
  • Dark mode is not available on login and other pre-authentication pages.
  • HTML report emails should stay in light mode for a better mailbox compatibility.

No matching browser preferences for now

The codebase is already prepared for browser-preference matching, but this PR does not expose that option to users yet.

  • auto theme mode support exists in the theme infrastructure.
  • The current user preference only exposes Light and Dark.
  • We want more coverage before enabling browser matching as a user-facing option.
  • Today, generated graphs can stay in the previous mode when the browser/OS preference changes.
  • In practice, those graphs need a page refresh or widget refresh to be redrawn in the new mode.

How it works

Theme values are now defined in PHP as either a single value or a [light, dark] pair. For example:

  /* before */
  public $colorText = '#212121';
  /* after */
  public $colorText = ['#212121', '#ccc'];

Those values are converted into CSS custom properties and LESS variables at asset-build time.

  • ThemeStyles::toLessCode() generates :root CSS custom properties for the light values.
  • It also generates dark overrides of this CSS custom properties for [data-theme-mode="dark"].
  • It also generates browser-matching dark overrides inside @media (prefers-color-scheme: dark) for [data-theme-mode="auto"].
  • The same method creates LESS aliases such as @theme-color-text: ~"var(--theme-color-text)".

This means most LESS can simply use shared theme tokens such as:

  color: @theme-color-text;
  background: @theme-color-background-contrast;
  border-color: @theme-color-border;

The active mode is selected through a root data attribute on the main layout:

  • data-theme-mode="light"
  • data-theme-mode="dark"
  • data-theme-mode="auto" (prepared, but not exposed yet)

The front end updates that attribute when the user saves the setting, so normal CSS-driven parts of the UI switch without a full reload.

We also added an .inDarkMode({ ... }) mixin for exceptions. Example:

.some-widget {
  .inDarkMode({
    filter: none;
    background: @theme-color-background-contrast;
  });

That mixin is useful for true mode-specific edge cases, but it should be the exception, not the default. The preferred approach is to share the same LESS rule in both modes by using theme variables, because that keeps the code smaller and reduces maintenance cost.

Side effects (why this PR is so big)

To make dark mode possible, this PR also cleans up older styling and assets.

  • Many hard-coded colors were migrated to theme variables.
  • In some places the new light-mode color is very close to, but not exactly, the previous literal color.
  • As a result, this PR changes a large number of screenshots with very small visual diffs.
  • Some PNG assets had to be cleaned up to remove solid white backgrounds and make them transparent.
  • Some small PNG icons were migrated to SVG so they render more cleanly across themes.
  • ExampleTheme support dark mode too so we can make dark UI Screenshots

Risks

  • ⚠️ This PR changes how theme values are exposed: theme LESS variables now resolve to CSS custom properties, so external theme plugins that apply LESS color functions such as lighten(), darken(), or fade() directly to theme variables will no longer compile and must be updated.
  • ⚠️ ThemeStyles is now effectively a broader public API change: many public properties changed from scalar strings to string|array, and the constructor now requires a mode argument. That is fine inside core, but it is a compatibility risk for
    extensions/themes that instantiate or inspect ThemeStyles directly.
  • Any remaining hard-coded color, image background, or legacy widget styling can stand out in dark mode.
  • Premium plugins and plugin submodules may still contain light-only styling and are not fully audited by this PR.
  • Contrast and accessibility still need continued validation on less-travelled admin pages and complex widgets.

In scope

Dashboard

Data:

  • Data tables
  • Table configuration (the controls used to customize a report table, such as columns, sorting, filters, and display options)
  • Limit selector (the selector that changes how many rows are displayed in a report or widget)
  • Sparklines
  • jqPlot-based graphs
  • Series picker
  • Single-metric visuals

Core dashboard UI:

  • CoreHome layout
  • Comparisons
  • Site selector
  • Segment Editor UI

Specific widget and data visualization:

  • Live / real-time UI
  • Visitor Map styling and related visual assets
  • Visitor profile related surfaces
  • Row evolution
  • Transitions popup/report styling

Shared UI primitives

  • Alerts (Info, Warning, Error)
  • Forms
  • some jQuery UI
  • Buttons
  • Cards
  • Panels
  • Navs
  • Popups
  • Expandable lists (Visible in the Segment Editor)
  • Autocomplete popups (Visible in the Segment Editor)

All Websites

  • MultiSites / All Websites visuals
  • Small trend/status icons migrated away from PNG-only rendering
  • Sparklines

Administration

Plugin and settings administration:

  • Plugin administration screens
  • Manage Plugins row states
  • Diagnostics config file page

Marketplace:

  • Marketplace plugin details styling

Known as out of scope

  • Table headings looks weird
  • Installation pages, which still use their own dedicated installation layout and CSS.
  • Login, invitation, password-reset, and other pre-authentication pages.
  • A full premium-plugin audit, but we already know some glitches:
    • Administration > Settings > SAML > Access to SP metadata > The Entity ID color field
    • Administration > Websites > Session recording > Create new session recording > URL validator helper loader
    • Administration > Websites > Custom Reports > Create new report > Dimensions & Metrics dropdown
    • Dashboard > Visitors > Real-time Map > the timing is hard to read (inverted box-shadow)
    • Dashboard > Funnels > See a Funnel > Heading in Funnel Report
    • TagManager > Create new container > Context title is dark
    • TagManager > Deep pages > a lot of tables or block are white / whity
    • Administration > Websites > Custom Reports > See a report > An extra white line at the left bottom of table
    • Dashboard > Behaviour > Users Flow > label are dark
    • Dashboard > Visitors > Cohorts > Cohorts Table

Plugins update (screenshots)

This PR will have impact on plugins UI screenshots:

Checklist

  • [NA] I have understood, reviewed, and tested all AI outputs before use
  • [NA] All AI instructions respect security, IP, and privacy rules

Review

@tzi tzi added Pull Request WIP Indicates the current pull request is still work in progress and not ready yet for a review. c: Design / UI For issues that impact Matomo's user interface or the design overall. labels Dec 23, 2025
@github-actions
Copy link
Copy Markdown
Contributor

If you don't want this PR to be closed automatically in 28 days then you need to assign the label 'Do not close'.

/**
* @var string|array<string>
*/
public $colorBoxShadow = ['rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.1)'];
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ℹ️ If we do a light box-shadow in dark mode, it doesn't look the same at all. More an highlight than stacking shadow.

$view->emailStyles = $emailStyles;

$view->fontStyle = 'color:' . $themeStyles->colorText . ';font-family:' . $themeStyles->fontFamilyBase . ';';
$view->fontStyle = 'color:' . $themeStyles->getPropertyValue('colorText') . ';font-family:' . $themeStyles->getPropertyValue('fontFamilyBase') . ';';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ℹ️ We avoid direct access to a color, because it could be defined as an array.

Comment on lines +13 to +31

.whatisnew .card {
.inDarkMode({
background: @theme-color-background-tinyContrast;
});
}

.whatisnew-changelist {
overflow-y: scroll;
max-height: 500px;
margin-bottom: 10px;
padding-right: 5px;
padding-left: 5px;
}

.whatisnew-btn {
float:right;
margin-left: 10px;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ℹ️ Moved from layout.less

padding: 15px;
}

.inDarkMode({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we not using variables in this file?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: Design / UI For issues that impact Matomo's user interface or the design overall. Do not close PRs with this label won't be marked as stale by the Close Stale Issues action Pull Request WIP Indicates the current pull request is still work in progress and not ready yet for a review. Stale The label used by the Close Stale Issues action

Development

Successfully merging this pull request may close these issues.

3 participants