Atlas Design — Make a Mendix App Look Designed, Not Bland

SkillMedia

Lets your agent style a Mendix app with polished layout, spacing, typography and colour so it looks professionally designed.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Atlas Design — Make a Mendix App Look Designed, Not Bland skill

About this capability

Make a Mendix app look designed rather than default-Atlas: layout, spacing, typography, colour and design properties that reach a finished standard. Use when asked to make an app look professional, branded or less bland, when styling pages, or when matching a design mock.

What this skill tells your AI

The instructions your AI receives, as published by mendixlabs/mxcli in .claude/skills/mendix/atlas-design/SKILL.md and read by ahel’s review.

Reference files

SKILL.md covers the thesis, the layer architecture, the workflow and the gotchas. The inventories are next door:

  • reference/building-blocks.md — what Atlas ships out of the box (layouts, page templates, building blocks, widgets) and the appearance vocabulary: the classes and design properties available on each. Look here before writing custom SCSS — most of what people hand-roll already exists as a class.
  • reference/dark-mode-and-charts.md — a dataviz-grade theme for the Mendix chart widgets, and the optional per-widget overrides that make dark mode look deliberate rather than inverted.

When to Use This Skill

Use this skill when:

  • The user asks to make an app "look good / professional / branded / less bland"
  • You are about to style a Mendix web app or a group of pages
  • You are matching a design mock and want it to reach "designed product" quality
  • You are re-branding an existing app to a new identity (palette, type, corners)

This is the taste + workflow layer. It sits on top of the styling mechanics (theme-styling), the widget syntax (create-page), the composition primitives (fragments), and the design-handoff pipeline (migrate-design-prototype). It does not re-teach SCSS compilation or Class:/DesignProperties: syntax — those skills own that. It adds which tokens/classes to use, when, and the discover → inspect → use method built on the Atlas building blocks every Mendix project already ships.

Contents

  1. The thesis: be Atlas-first
  2. The 4-layer architecture
  3. The workflow: discover → inspect → use
  4. Brand re-tune (Layer 1) — where most of the win is
  5. Layer 1 in practice — start from the shipped theme
  6. Dark mode — Mendix 11 makes this cheap
  7. Verify at runtime — this is mandatory
  8. Gotchas catalog
  9. Validation checklist
  10. Related skills

Inventories and the two long theming sections live beside this file — see Reference files above.


The thesis: be Atlas-first

Every Mendix project ships Atlas — a rich appearance system (Atlas_Core classes + typed design properties) and 39 out-of-the-box building blocks (Atlas_Web_Content: cards, headers, forms, lists, timelines, wizards, alerts). The single biggest mistake is hand-rolling .panel / .trip-card / .stat SCSS that reinvents what Atlas already gives you for free.

Live testing proved the point: a page of pure Atlas classes, zero custom CSS renders real cards, brand-coloured backgrounds and buttons, and flex layouts — and those Atlas utilities inherit your retuned brand tokens automatically (background-primary resolves to your --brand-primary).

Reach down the stack first. Need a card? class:'card' (or 'Card style': on) before writing a .panel rule. Brand blue on a button? Retune --brand-primary before overriding .btn-primary. Custom CSS is the last resort — for identity only (a mono metric type, a timeline spine, a bespoke elevation curve).


The 4-layer architecture

Style from the bottom up. Each layer only does what the layer below can't.

Layer 3  VERIFY      run --local --watch  +  Playwright screenshot   (mx check is NOT enough)
Layer 2  IDENTITY    theme/web/_<name>.scss, imported from theme/web/main.scss — recipe
                     classes (mono type, status pills, timeline spine) — ONLY what Atlas can't do
Layer 1  BRAND       theme/web/custom-variables.scss — retune Atlas tokens (--brand-primary,
                     backgrounds, semantic colors, radius) so Atlas components inherit the palette
Layer 0  ATLAS       Atlas classes / design properties / building blocks — structure & base look
  • Layer 0 — Atlas. Compose with the Atlas vocabulary (the class cheat-sheet and the building-block inventory below).
  • Layer 1 — Brand. Retune Atlas tokens in theme/web/custom-variables.scss so the whole framework (buttons, backgrounds, form inputs, pluggable widgets like Switch/Slider/ProgressBar) picks up your palette. Start from the shipped theme rather than a blank file — see below.
  • Layer 2 — Identity. Only the handful of shapes Atlas genuinely can't express. Put them in a partial imported from theme/web/main.scss, which compiles last — after Atlas Core and after every module theme source — so your rules win without !important. Use themesource/<mod>/web/main.scss only when the styling belongs to that module: a theme source folder whose name does not match a real module is silently not compiled. See theme-styling.
  • Layer 3 — Verify. Non-negotiable. mx check misses client-side crashes; you must screenshot a running build.

Start from the shipped default, don't start from nothing. mxcli new applies the signal theme, and mxcli theme apply -p app.mpr adds one (signal, ledger or console) to an existing project. Each carries a full palette in both light and dark, vendored fonts, the focus ring, the density scale and the num / pill / stat recipe classes. mxcli theme show <name> lists exactly which files it writes, and the --mxt-* vocabulary a palette is made of.

Two ways to re-brand, and picking the wrong one costs you the theme. The generated blocks are digest-fenced: an edit inside one is refused on the next apply rather than discarded. That protects your work, but it also means the project has taken the theme out of mxcli's hands.

  • Changing one or two values — a brand colour, a radius: edit them in the palette block and accept that theme apply will now report the file as modified. Fine for a tweak.
  • A real brand — your palette, your type, your density: mxcli theme create. It scaffolds a theme the project owns, which theme list -p shows, theme apply <name> installs and theme remove takes out — no fence to fight.
mxcli theme create acme -p app.mpr --from design/tokens.css
mxcli theme apply acme -p app.mpr

A Layer-1 token retune cascades down into Atlas components and pluggable widgets for free — that is the headline payoff. A full re-brand (new palette, type, corners) is theme-only: retune custom-variables.scss + main.scss, zero page/MDL edits, and it hot-applies under --watch.


The workflow: discover → inspect → use

Building blocks are the Mendix-native recipe library. mxcli can read and instantiate them, so the workflow is:

1. Discover what your project ships.

mxcli -p app.mpr -c "show building blocks"
mxcli -p app.mpr -c "show building blocks in Atlas_Web_Content"
mxcli -p app.mpr -c "select QualifiedName, Category from CATALOG.building_blocks"

2. Inspect the block you want to reproduce. describe prints its real widget tree — the exact classes and typed design properties Mendix itself uses:

mxcli -p app.mpr -c "describe building block Atlas_Web_Content.Card"
{
  container container2 (DesignProperties: ['Card style': on]) {
    dynamictext text22 (Content: 'Card title', RenderMode: H4, Class: 'card-title',
      DesignProperties: ['Spacing': ['margin-bottom': 'L']])
  }
}

Note the two styling channels Atlas uses side by side: the Class: vocabulary (card-title) and typed DesignProperties: ('Card style': on, Spacing).

3. Use it — one line. use building block deep-copies the block's widget tree onto your page, exactly like dragging it in from the Studio Pro toolbox. Add as <prefix> to rename the copied widgets (so you can drop the same block in twice):

use building block Atlas_Web_Content.Card as cust_

That expands to the exact tree DESCRIBE showed — here cust_container2 + cust_text22, carrying the card-title class and the Card style design property. It's a page-body element: put it inside a create page / alter page container, anywhere a widget or use fragment can go.

4. Configure the copy afterwards. A building block has no parameters — it's a raw widget-tree template — so you bind data / set text by editing the copied widgets with alter page (their names are deterministic thanks to the prefix):

alter page Sales.CustomerOverview set cust_text22 (content: 'Customers');

Capability reality. Discovery (SHOW/DESCRIBE BUILDING BLOCK, CATALOG.building_blocks) and instantiation (USE BUILDING BLOCK) both work today. use building block v1 is deep-copy + optional as <prefix>; configure the copy afterwards with alter page (an inline override block is a proposed v1.1).

When to mirror instead. Mirroring — reproducing a block's tree by hand with create page/alter page + the same classes and design properties (see below) — is the fallback: reach for it only to hand-tune a shape Atlas doesn't quite give you. Otherwise prefer the one-line use building block.


Brand re-tune (Layer 1) — where most of the win is

Retune the palette in theme/web/custom-variables.scss — the file mxcli theme apply writes (see the next section; do not hand-roll one). Because Atlas utilities and pluggable widgets read these tokens, one retune re-skins the whole app:

  • --brand-primary → buttons, background-primary, links, Switch/Slider/ProgressBar
  • background + semantic (success/warning/danger) tokens → alerts, group boxes, status backgrounds
  • --card-border-radius and radius tokens → cards, inputs, popups (drop to 0 for a sharp, industrial identity; raise for a soft, friendly one)

Only after the token retune, reach for Layer-2 identity classes in main.scss — and only for shapes Atlas can't provide.


Layer 1 in practice — start from the shipped theme

Do not hand-roll a brand scaffold. mxcli theme apply -p app.mpr writes a complete, verified Layer 1 (and Layer 2) into theme/web/, and mxcli new applies one by default. Re-brand it instead of competing with it — the generated blocks are digest-fenced, so a hand-written palette in the same file will either be refused on the next apply or silently fight the theme in the cascade.

mxcli theme list -p app.mpr            # built-ins + this project's own themes
mxcli theme show signal                # palette, files it writes, token vocabulary
mxcli theme apply signal -p app.mpr    # --variant auto | light | dark

When the brand is genuinely yours, make it a theme rather than an edit:

mxcli theme create acme -p app.mpr                    # scaffold from signal
mxcli theme create acme -p app.mpr --from console     # ...or from console
mxcli theme create acme -p app.mpr --from design.css  # ...and seed the palette
mxcli theme apply acme -p app.mpr

It lands in theme/mxcli-themes/<name>/ — committed, and not compiled until apply copies it into theme/web/. Scaffolding copies an existing theme, so the Atlas map, the recipe layer and the widget layer come across byte for byte; what you edit is the palette. --from <file> reads --mxt-* declarations out of any CSS-shaped text, filing a prefers-color-scheme: dark block into the dark palette. A --mxt-* name the base theme does not declare is refused, because nothing would read it — the theme would apply cleanly and render unchanged.

Several themes can be installed at once and switched by a class on <html>:

mxcli theme apply signal ledger console -p app.mpr   # first named is the default
mxcli theme switcher install -p app.mpr --module MyFirstModule

The token architecture it gives you

A theme separates the palette from the wiring, and that split is the whole reason a light/dark flip or a re-brand is cheap:

FileHoldsYou edit
theme/web/custom-variables.scssthe palette — --mxt-* tokens for the default variantyes, this one
theme/web/_mxcli-atlas-map.scss~60 Atlas variables expressed as var(--mxt-*)no
theme/web/_mxcli-<name>.scssthe other palette, variant blocks, @font-face, recipe classesrarely

To re-brand, change one line in the palette:

:root {
  --mxt-brand: #0f6e6b;      /* the one colour that defines the app */
  --mxt-ground: #f4f6f8;     /* app background */
  --mxt-surface: #ffffff;    /* cards, modals, panels */
  --mxt-ink: #14181f;        /* primary text */
  --mxt-line: #dce1e7;       /* hairlines */
}

Atlas derives --brand-primary-50 … -900 from --brand-primary with CSS color-mix(), so buttons, links, active navigation, alerts, group boxes and the brand-aware pluggable widgets (Switch, Slider, RangeSlider, ProgressBar, ProgressCircle, BadgeButton) all follow — in both palettes, with no per-widget CSS.

Two rules that decide whether your styling survives

  1. Mendix 11 Atlas is CSS-custom-property-first. Write :root { --x: … } declarations, not SCSS $x: … !default;. The stock custom-variables.scss is a :root block plus a few SCSS switches ($font-family-import, $btn-bordered, $use-css-variables); legacy Sass variables are still mapped for old modules, but they are not the idiom.
  2. Never pin an Atlas variable to a literal colour. Map it to a token (--bg-color: var(--mxt-ground)), which is what the Atlas map does. A hardcoded --font-color-default is near-black on a near-black ground the moment anything flips the palette — the failure is total and silent.

If you genuinely need a token the theme does not expose, add it to the palette block and reference it from your own Layer-2 rules. See theme-styling for the compile order and for why theme/web/main.scss is the only correct home for app-level rules.


Dark mode — Mendix 11 makes this cheap

Older guidance here said to commit to a single theme, because a prefers-color-scheme flip repainted your own classes but left Atlas widgets light. That was Atlas 3. It does not hold on Mendix 11.

Measured by adding theme-dark to <html> on a running 11.13 app and changing nothing else: the page ground, cards, form controls, sidebar, buttons and DataGrid2 all followed. Atlas is CSS-custom-property-first now, so the token cascade genuinely propagates. And because the class lands on <html>, popups and modals — which Mendix renders at <body>, outside any page container — follow it too, which was the other half of the old objection.

The practical route is mxcli theme apply <name> with the default --variant auto: it ships both palettes, follows the OS before first paint, and honours a theme-light / theme-dark class when a switcher sets one. Add mxcli theme switcher install for a user-facing toggle.

Three things to know if you build this by hand:

  1. Mendix ships the slot, not the switcher. theme/web/_theme-dark.scss declares :root.theme-dark; nothing in Atlas ever applies the class.
  2. Your dark block must come after Mendix's — same specificity, later wins. Otherwise its stock Mendix blue overrides your brand the moment the class appears.
  3. Anything you pinned to a literal colour breaks. This is the whole reason Layer 1 maps Atlas variables to tokens instead of to hex values.

The rail is the one place Atlas still assumes: several topbar widgets paint text with --color-base, expecting white because they expect a dark navigation rail. Keep the rail dark in both palettes, or force color: inherit on those widgets.

Charts remain the exception — series colour lives in the model (customSeriesOptions), not CSS, so it does not follow a runtime flip. Use the transparent paper_bgcolor trick above, which is correct in both palettes.

The override sheet below is still useful for a hand-rolled dark theme, or for Atlas corners a token flip misses.


Verify at runtime — this is mandatory

Runtime verification is not optional. mx check (and mxcli check --references) validate the model — they pass MDL the browser client still crashes on:

  • an old ListView carrying SearchRefs the client can't render;
  • the Slider / RangeSlider tooltip calling React's removed findDOMNode — this only throws on drag, so a static check (even a static screenshot) misses it;
  • a structural change that leaves the client bundle unbuilt (blank <noscript> shell).

A model that checks clean can still render a white page. Never ship on mx check alone. Keep the app hot and screenshot every change:

mxcli run --local -p app.mpr --watch --screenshot
  • SCSS / theme edits hot-apply (~1 s) — no restart. Layer-1 (custom-variables.scss) and Layer-2 (main.scss) both reflect on the next shot.
  • Page / microflow / text edits hot-apply too (reload_model, ~1 s).
  • Structural changes restart + DDL (~9 s): a new entity, view entity, or association is reconciled only at runtime startup, so run --local restarts automatically. A hot reload won't see a new entity — expect the restart.
  • --screenshot writes a Playwright PNG (default <projectDir>/.mxcli/run-local.png) after boot and after each applied change.
  • --screenshot-url /p/customers targets a specific page (repeatable — one PNG each).
  • --screenshot-user / --screenshot-password log in once for pages behind login.

From an egress-only environment (Claude Code web): --hub <url> reverse-tunnels the local app out over a single 443 connection to a relay, giving a public URL you can open in a real browser. --hub implies --local. See run-local for the flags.

What a screenshot can't catch — drive the interaction. A single screenshot is a static frame; the Slider findDOMNode throw fires on drag, a filter popover's white gradient only shows when opened. For interactive widgets, either screenshot the interacted state or set the safe default up front (Slider showTooltip: false).

The rhythm: keep terminal 1 hot (run --local --watch --screenshot); in terminal 2 apply one slice (mxcli exec 06-redesign.mdl -p app.mpr) and look at the PNG. A designed result is reached by looking at the running app, not by trusting the checker.


Gotchas catalog

Each cost real time in the builds this skill was distilled from. Match a symptom to a row before opening files.

Styling & pages

GotchaFix
$ in dynamictext content: breaks the parser (starts a variable token)put the $ in CSS ::before; bind only the number
Enum dynamictext renders the key, not the captionaccept it, or map the enum to a class via dynamicclasses
sort by not allowed on association-sourced listviewssort the parent, or use a DB datasource
Reserved widget identifiers exist (e.g. v3)prefix names (sv3); avoid bare v<n>
Pluggable widgets impose their own DOM (charts / timeline / treenode)for pixel-fidelity use a native listview / gallery you fully style
Inline style: on a dynamictext crashes MxBuild (NullReferenceException)use class:, or wrap the text in a styled container
alter styling can't find widgets in MDL-builder-created pagesapply classes via Class: / DynamicClasses: in create page / alter page
Full-screen page wanted (no Atlas sidebar) but no blank layout resolveskeep a normal Atlas layout; hide the shell per-page with .mx-page:has(.my-app) .region-sidebar { display:none }
"Colour by state" (status pills / cards)one dynamicclasses enum→class expression + one --st CSS var cascaded into pill/number/dot/border

Charts

GotchaFix
Chart widgets render raw Plotly defaults (flat colour, floating mode-bar, white paper, heavy grid)customLayout (transparent bg + system font + faint grid) + customConfigurations displayModeBar:false + per-series customSeriesOptions (colour, cornerradius, spline)
Horizontal BarChart with aggregationType: sum prepends a 0 group-key to category ticks ("0Tokyo Spring")use aggregationType: none when the datasource is already one row per category
Chart colours don't re-skin — series colour lives in the model (customSeriesOptions), not CSSaccept it's model config; a palette pivot needs an MDL edit + gen-2 restart, not a theme edit

Dark mode & widgets

GotchaFix
Atlas widgets + Plotly aren't dark-aware — a prefers-color-scheme flip leaves them light on a dark pageship the dark-mode override block above (form controls, datagrid + filters/popovers, accordion, fieldset, treenode white rows, transparent charts), or ship light-only
.widget-dropdown-filter-menu paints a hardcoded white scroll-fade gradient even after bg is themedoverride background-image:none and brighten menu-item text
Edit popup has a white title bar — .mx-window / .modal-content renders at <body>, outside your scoped classtheme .mx-window-content / .modal-content + header + form controls/buttons globally, not scoped
Slider / RangeSlider throw "Could not render widget" on drag (tooltip calls React findDOMNode, removed in MX 11)set showTooltip: false
Half-dark clash (your chrome dark, Atlas widgets light)commit to one theme: for a dark app drop the media gate and make overrides unconditional + global; ship light-only if you can't fund the override recipe

Theme / SCSS

GotchaFix
Google-fonts @import url() silently droppedmake it the first line of main.scss (before the partial import and any rule); keep a system fallback stack
Full re-skin desired (new identity)it's theme-only — retune custom-variables.scss (Atlas leaves) + main.scss (custom tokens + classes); no page/MDL edits, hot-applies under --watch
"SCSS cache" — edits don't showit's never a cache: use --watch (watches theme source) or a clean restart; kill any stale process first
Stale process serves old output, looks like a cacherun --local refuses occupied ports; free them (pgrep/kill, curl returns 000 when down)

Data / microflows behind the design (styling depends on real data)

GotchaFix
Seed microflow data doesn't appear (queries empty)create doesn't persist — add commit $obj;; the miss is silent (no error)
Bare $x = avg(...) or $x = 2 fails to parsebare $x = … accepts only count/sum aggregates; use declare $x T = expr for other expressions, set $x = expr to reassign
Aggregates can't be inlined in a create-object assignment (CE0117)compute into vars first
Integer/integer division $a / $b → CE0117Mendix / needs a decimal operand; compute upstream or store decimals
View entity flagged CE6770 "out of sync"the view's declared attribute types must match its OQL source columns; a grouped enum column must be typed enumeration(Module.Enum), not string

Verify

GotchaFix
mx check passes but the browser client crashes (old ListView SearchRefs; the Slider findDOMNode throw only fires on interaction)always Playwright-verify a running build; never ship on mx check alone
ALTER PAGE SET layout … map(…) swaps a page onto a sidebar shellit does so without rebuilding the widget tree — use it to re-parent, not to rebuild

Validation Checklist

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
122
Forks
49
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
atlas-design
Source
github.com/mendixlabs/mxcli