Default Styling (mxcli theme)
A blank Mendix app looks like a blank Mendix app. mxcli new applies a default
theme so a generated app looks like a product on first boot, and
mxcli theme apply adds the same theme to a project you already have.
mxcli theme list # built-in themes; the default is marked *
mxcli theme show signal # palette, colorway, and the files it writes
mxcli theme apply -p app.mpr # apply to an existing project
mxcli theme apply ledger -p app.mpr # switch theme (the previous one is removed)
mxcli theme apply -p app.mpr --dry-run # report changes without writing
mxcli theme remove -p app.mpr # take it back out
mxcli new MyApp --version 11.13.0 # applies `signal`
mxcli new MyApp --version 11.13.0 --theme none # plain Atlas
The themes
| Name | Default palette | Character |
|---|---|---|
| signal (default) | light | Cool slate, one teal signal colour, 4px radius, 32px rows, IBM Plex |
| ledger | light | Warm paper, hairline rules instead of card shadows, Source Serif headings over Source Sans, 2px radius, 30px rows |
| console | dark | Near-black ground, teal with a violet accent, Space Grotesk over JetBrains Mono, 6px radius, 28px rows, surfaces separated by lightness |
All three share the same density discipline: an 8px spacing unit, monospace numerics, a visible focus ring on every focusable element, and every control growing to a 44px touch target below 768px.
Only one theme applies at a time — theme apply removes the previous one,
because two themes mapping the same Atlas variables would fight in the cascade.
What it writes
Six things, all under theme/:
| File | What |
|---|---|
theme/web/custom-variables.scss | the theme’s palette — this is the file to edit |
theme/web/_mxcli-atlas-map.scss | the Atlas wiring: ~60 Atlas variables expressed in terms of the palette |
theme/web/_mxcli-<name>.scss | the other palette, the variant blocks, @font-face, recipe classes |
theme/web/_mxcli-widgets.scss | the widget-module layer: colours Sass bakes before any token exists |
theme/web/main.scss | the variant switch plus the @import lines |
theme/web/mxcli-fonts/ | vendored fonts (SIL OFL 1.1) |
The model is never touched. No .mpr changes, so nothing here can affect a
build, and the theme hot-applies under mxcli run --local --watch.
Atlas Core is never touched either. Because Atlas components read the brand tokens, retuning them cascades into buttons, backgrounds, form inputs, cards, modals and the brand-aware pluggable widgets (Switch, Slider, ProgressBar, BadgeButton) with no per-widget CSS. That is also why the project stays upgradable across Mendix releases.
Light and dark
--variant auto is the default and ships both palettes:
mxcli theme apply signal -p app.mpr # auto
mxcli theme apply signal -p app.mpr --variant dark # bake one palette, no switching
Under auto the app follows the operating system’s prefers-color-scheme
before first paint — no flash, no script — and honours a theme-light or
theme-dark class on the root element when something sets one.
Mendix ships that slot (theme/web/_theme-dark.scss declares :root.theme-dark)
but nothing that applies it, and its palette is stock Mendix blue. An mxcli theme
re-declares the same selector from a file that compiles later, so the theme’s own
dark palette wins.
A user-facing toggle
mxcli theme switcher install -p app.mpr --module MyFirstModule
This is the one theme command that writes to the model. It has to: the class
has to be set by something the browser can run, and there is no theme-level hook
to run script before first paint. It creates three JavaScript actions
(ToggleAppTheme, SetAppTheme, ApplyStoredTheme) and a nanoflow, then you
wire a button:
actionbutton btnTheme (caption: 'Theme', action: nanoflow MyFirstModule.ACT_ToggleTheme)
A click flips the palette and remembers the choice in localStorage. The class
goes on <html>, so popups and modals — which Mendix renders at <body>,
outside any page container — follow it too.
Known limit: after a reload the app goes back to following the OS. Mendix has
no page on-load event to re-apply the stored value, and the usual substitute (a
data view with a nanoflow data source) is not authorable by mxcli on either
engine yet. ApplyStoredTheme is installed and ready — wire it in Studio Pro if
you need the choice to persist across reloads.
Re-branding
Change one line in theme/web/custom-variables.scss:
:root {
--mxt-brand: #0f6e6b; /* <- the one signal colour */
_mxcli-atlas-map.scss maps that onto --brand-primary, and Atlas builds the
whole derived ramp (--brand-primary-50 … -900) from it with CSS
color-mix() — so buttons, links, active navigation and the brand-aware
pluggable widgets follow immediately, in both palettes.
The palette file declares only --mxt-* tokens, never Atlas variables directly.
That is what makes a variant cheap: the dark block restates about thirty values
instead of rewiring sixty. Pinning an Atlas variable to a literal colour is the
one thing that breaks switching — a hardcoded --font-color-default is
invisible the moment the ground goes dark.
Import order in main.scss
apply appends its block to the end of theme/web/main.scss, so it lands
after any @import the project already had there. If your app carries a
stylesheet imported last specifically to win the cascade over Atlas, the theme’s
partial now comes after it. Higher-specificity app classes are unaffected; a
rule that relied purely on being last is not. Move your @import below the
mxcli block — anything outside the fence is never touched.
Two Atlas constraints worth knowing
- The navigation rail stays dark in both palettes. Atlas topbar widgets paint
their own text assuming a dark rail — the language selector uses
--bg-color-secondarywith a#ffffallback, at a specificity (.navbar-brand .widget-language-selector .current-language-text) that a simple override cannot beat. Every mxcli theme keeps the rail dark and re-declares those selectors at matching specificity, resolving the colour through the rail token. themesource/<name>/is only compiled when<name>matches a real module, so a theme never writes there.theme/web/main.scsscompiles last and is the correct home for app-level styling.- The widget modules bake some colours as Sass literals, before any custom
property exists, so no token can move them — the Data Grid 2 pager caption is
the worst case, at 1.02:1 on a dark ground.
_mxcli-widgets.scsscorrects those with ordinary rules; it is regenerated with the theme, so leave it alone and put your own overrides outside the fence.
Recipe classes
Apply with class: on any widget.
| Class | Use |
|---|---|
num | monospace with tabular figures — ids, amounts, dates, so columns align |
num-right | right-align a numeric column |
pill + pill-ok / pill-warn / pill-risk / pill-info | status pills; pair with a dynamicclasses expression mapping an enum |
stat + stat-label / stat-value / stat-delta / stat-delta-up / stat-delta-down | KPI tile |
density-compact | 28px rows and inputs inside this container |
Your edits are safe
Every generated region is fenced, and the closing marker records a digest of what mxcli wrote:
// mxcli:theme:begin signal v1 — generated by `mxcli theme apply`; edit outside this block
…
// mxcli:theme:end signal b538a13336af87f6
- Edit outside the markers and mxcli never touches your lines.
- Edit inside them and the next
applyrefuses rather than discarding your work, naming the file and offering--force. applyis idempotent — re-running an unchanged theme reportsunchangedfor every file.removecuts the blocks back out and restores the file byte for byte.
Verifying
A theme that compiles cleanly can still render wrong, so check a running app rather than the build log:
mxcli run --local --watch --screenshot -p app.mpr
SCSS edits hot-apply, so --watch gives you a tight loop. If a rule seems not to
apply, first confirm it is compiled at all — grep for the selector in
theme-cache/web/theme.compiled.css. Absent and overridden look identical in the
browser, and only one of them is a specificity problem.
See also the atlas-design skill for the method behind the theme, and
theme-styling for the SCSS compilation chain — both are installed into your
project by mxcli init.