--- name: theme-styling description: "The SCSS workflow and its traps — where styling actually compiles, custom-variables.scss, themesource directories, hot reload, design-property errors, and the mxcli theme commands (apply, create --from a design, switchable sets, light/dark). Use when writing or debugging SCSS, when applying or building a theme, when giving an app a brand palette or design tokens, or when styling silently fails to appear." --- # Theme & Styling — SCSS Workflow and Caveats ## When to Use This Skill Use this skill when working with: - SCSS compilation, `custom-variables.scss`, or `themesource/` directories - Applying or building a theme (`mxcli theme apply | create | switcher`) - Giving an app a brand palette, or turning design tokens into a theme - CSS hot-reload during Docker development - Debugging styling crashes or design property issues **Do not hand-write a theme scaffold.** `mxcli theme apply` writes a complete, verified one; `mxcli theme create --from ` makes a theme the project owns and seeds its palette from `--mxt-*` declarations. See "A theme of your own" below. Hand-editing inside a generated block works exactly once — the digest fence refuses it on the next apply. For **MDL styling commands** (`list design properties`, `describe styling`, `alter styling`, inline `designproperties:`, `update widgets`), see: - Existing proposal: `docs/11-proposals/page-styling-support.md` - Working examples: `mdl-examples/doctype-tests/12-styling-examples.mdl` (595 lines) - Implementation: `mdl/executor/cmd_styling.go`, `mdl/executor/theme_reader.go` ## SCSS Compilation Chain ### Directory Structure ``` MyProject/ ├── theme/ # project-level overrides │ └── web/ │ ├── main.scss # SCSS entry point (import chain) │ ├── custom-variables.scss # project variable overrides │ ├── exclusion-variables.scss # Exclude unwanted Atlas components │ └── settings.json # Theme settings │ ├── themesource/ # module-level theme definitions │ ├── atlas_core/ # base framework (always present) │ │ └── web/ │ │ ├── design-properties.json # widget design properties │ │ ├── variables.scss # Color/spacing/font variables │ │ └── ... # Component SCSS files │ ├── datawidgets/ # DataGrid2, gallery, etc. │ ├── atlas_web_content/ # Web content styles │ └── / # Each module can contribute styles │ └── web/design-properties.json │ └── theme-cache/web/ # Compiled CSS output (build artifact) ``` ### Compilation Order `atlas_core/web/main.scss` imports in order: 1. Default variables (`atlas_core`) 2. Exclusion variables (disable Atlas components) 3. Project custom variables (`theme/web/custom-variables.scss`) 4. Bootstrap framework 5. MXUI components 6. Core styles (base, animations, spacing, flex) 7. Widget-specific styles Then each **module's** `themesource//web/main.scss`, and **last of all** `theme/web/main.scss`. Variables declared earlier are overridden by later declarations (with `!default` flag). This means `custom-variables.scss` overrides `atlas_core/web/variables.scss` values. ### Where to put app-level styling — three rules that are not obvious Verified against a real Mendix 11.13 project (probe rules compiled with `mxbuild --target=deploy`, then grepped out of `theme-cache/web/theme.compiled.css`). **1. `theme/web/main.scss` compiles LAST — it is the right home for app styling.** After Atlas Core *and* after every module theme source, so a partial imported here overrides any Atlas rule with **no `!important`**. It is a three-line file of Mendix's own imports, not an Atlas-owned file; appending one `@import` is safe: ```scss @import "custom-variables"; @import "theme-dark"; @import "theme-neutral"; @import "my-app"; // -> theme/web/_my-app.scss ``` **2. A `themesource//` folder is only compiled when `` is a real module.** mxbuild walks the model's modules and pulls each one's theme source; it never globs the directory. An invented folder (`themesource/my_theme/`) is **silently skipped** — build succeeds, rules simply absent. Use a module's theme source only when the styling belongs to that module (it then exports with the `.mpk`). > Debugging "my CSS doesn't apply": first prove the file is compiled *at all* — > grep a unique probe 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. **3. `theme/web/custom-variables.scss` is imported once PER MODULE** (8× in a blank app). It must hold **declarations only** — a CSS rule there is emitted once per module. Tokens go here; rules go in the Layer-2 partial. ### Mendix 11: CSS custom properties, not SCSS variables The stock `theme/web/custom-variables.scss` is a `:root { --brand-primary: … }` block plus a few SCSS switches (`$font-family-import`, `$btn-bordered`, `$use-css-variables`). Legacy Sass variables are still mapped (`_css-variables-mappings.scss`), but the modern idiom is `:root` declarations. The derived ramp (`--brand-primary-50…900`) is built with CSS `color-mix()` against `var(--brand-primary)`, so retuning the primary re-derives the whole ramp live — no SCSS recompilation of variants needed. ### Fonts: vendor them under `theme/web/` `theme/web//` is copied to the deployment web root, and `theme.compiled.css` is served from that root — so fonts at `theme/web/fonts/x.woff2` are referenced as `url("./fonts/x.woff2")`. Prefer this over `@import url('…fonts.googleapis…')`: no `@import`-ordering trap, no third-party request per page load, and the app renders correctly air-gapped. `mxcli theme apply` does exactly this — see `mxcli theme show signal`. ### A theme of your own: `mxcli theme create` Don't hand-edit a generated block to get a brand palette. The block is digest-fenced, so the next `theme apply` refuses to touch it and reports your file as modified — you have taken the theme out of mxcli's hands to change one colour. Scaffold a theme the project owns instead: ```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 (unlike `.mxcli/`, which `mxcli init` gitignores) and not compiled (mxbuild's entry point is `theme/web/main.scss`; it does not glob `theme/`). From then on it is a theme like any other: `theme list -p` shows it marked `local`, `theme apply` installs it, `theme remove` takes it out. A local theme named after a built-in shadows it. **Seeding from a design.** `--from ` reads `--mxt-*` declarations out of any CSS-shaped text — a stylesheet, an SCSS partial, or the `