--- name: atlas-design description: "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." --- # Atlas Design — Make a Mendix App Look Designed, Not Bland ## Reference files `SKILL.md` covers the thesis, the layer architecture, the workflow and the gotchas. The inventories are next door: - [`reference/building-blocks.md`](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`](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](#the-thesis-be-atlas-first) 2. [The 4-layer architecture](#the-4-layer-architecture) 3. [The workflow: discover → inspect → use](#the-workflow-discover--inspect--use) 4. [Brand re-tune (Layer 1) — where most of the win is](#brand-re-tune-layer-1--where-most-of-the-win-is) 5. [Layer 1 in practice — start from the shipped theme](#layer-1-in-practice--start-from-the-shipped-theme) 6. [Dark mode — Mendix 11 makes this cheap](#dark-mode--mendix-11-makes-this-cheap) 7. [Verify at runtime — this is mandatory](#verify-at-runtime--this-is-mandatory) 8. [Gotchas catalog](#gotchas-catalog) 9. [Validation checklist](#validation-checklist) 10. [Related skills](#related-skills) Inventories and the two long theming sections live beside this file — see [Reference files](#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/_.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//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 ` 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 ` installs and `theme remove` takes out — no fence to fight. ```bash 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.** ```bash mxcli -p app.mpr -c "list building blocks" mxcli -p app.mpr -c "list 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: ```bash 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 ` to rename the copied widgets (so you can drop the same block in twice): ```mdl 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): ```mdl 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 `**; 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. ```bash 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: ```bash 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//` — 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 ` 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 ``: ```bash 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: | File | Holds | You edit | |---|---|---| | `theme/web/custom-variables.scss` | the palette — `--mxt-*` tokens for the default variant | **yes, this one** | | `theme/web/_mxcli-atlas-map.scss` | ~60 Atlas variables expressed as `var(--mxt-*)` | no | | `theme/web/_mxcli-.scss` | the other palette, variant blocks, `@font-face`, recipe classes | rarely | To re-brand, change one line in the palette: ```scss :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 `` 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 ``, popups and modals — which Mendix renders at ``, outside any page container — follow it too, which was the other half of the old objection. The practical route is `mxcli theme apply ` 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 `