Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Theme Styling

ASecurity

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.

128 stars
0 votes
0 copies
0 views
Added 9/26/2026
ai-agentsjavascriptgojavabashsqldockerdebugginggitapibackend

Works with

cliapimcp

Security Analysis

A100/100

Scanned 10/5/2026

$npx -y skills add mendixlabs/mxcli --skill theme-styling --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Theme Styling?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Theme Styling
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mendixlabs-theme-styling/badge)](https://www.skillsdirectory.com/skills/mendixlabs-theme-styling)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
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 <name> --from <design-file>` 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
│   └── <module_name>/              # 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/<module>/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/<name>/` folder is only compiled when `<name>` 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/<subdir>/` 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/<name>/` — 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 <file>` reads `--mxt-*` declarations out of
any CSS-shaped text — a stylesheet, an SCSS partial, or the `<style>` blocks of
an HTML export:

```css
:root { --mxt-brand: #7f5af0; --mxt-ground: #fffffe; }
@media (prefers-color-scheme: dark) { :root { --mxt-ground: #16161a; } }
```

A dark block (`prefers-color-scheme: dark`, `.theme-dark`, `[data-theme="dark"]`)
seeds the dark palette; everything else seeds the light one. Tokens the design
does not name keep the base theme's value. **A design with only a base palette
does not touch the other variant** — it keeps the base theme's palette, and
`create` says so. To retheme dark mode too, emit a dark block (a `light` one for
a dark-first base such as console).

If you are driving a design step (`/design` or similar) that will feed this, ask
it to **emit a token block** rather than inferring one from the mockup. Two greys
in a design do not say which is the app ground and which is a hovered row;
`mxcli theme show signal` prints the exact vocabulary to target. 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 at once — the app switches skins at runtime

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

All of them compile into one stylesheet; the app picks one with a class on
`<html>`. No rebuild, no reload. This is the CSS Zen Garden result for Mendix:
the DOM Mendix renders never changes and neither does the model — brand, ground,
ink, radius, type and card treatment all move on a class swap. Measured on a real
11.13 app: signal `#0f6e6b`/4px/IBM Plex with shadowed cards, ledger
`#1f3a5f`/2px/Source Sans with hairlines, console `#2dd4bf`/6px/Space Grotesk
flat.

Two things make it work, and both are worth knowing if you write a theme:

1. **Nothing outside the palette may name a colour.** The Atlas map, the recipe
   layer and the widget layer resolve everything through `var(--mxt-*)`, so one
   copy of them serves every theme. A literal in any of those files survives the
   swap and is wrong under every theme but one.
2. **The default theme's scope is `:root` minus the other skins' classes**, not a
   bare `:root`. Bare keeps matching once another class is set, so its rules leak
   under every other theme and the winner comes down to specificity. Negation
   makes the scopes mutually exclusive.

A single installed theme is emitted exactly as before — bare `:root`, skin rules
unscoped — so this costs a one-theme project nothing.

### A JavaScript action's parameter arrives lowercased

Not theme-specific, but the theme switcher is where it bit. mxbuild **lowers the
first letter** of a parameter when it generates the action wrapper:

```
create or modify javascript action Mod.SetAppSkin(Skin: String) ...
```
```javascript
// javascriptsource/mod/actions/SetAppSkin.js — regenerated on every build
export async function SetAppSkin(skin) {      // ← lowercased
```

So the body must read `skin`, not `Skin`. Using the modelled spelling is a
`ReferenceError` on the first click, and **nothing catches it before then**:
`mx check` reports 0 errors because the action is well-formed and the body is
opaque user code, `mxcli check` says nothing about JavaScript, and the file
carries `Only the following code will be retained` — it is rewritten on every
build, so it cannot be patched in place. The fix has to go back through MDL.

Model the parameter capitalised, as Mendix does; read it lowercased.

### Light/dark: Mendix ships the slot, not the switcher

`theme/web/_theme-dark.scss` and `_theme-neutral.scss` declare `:root.theme-dark`
and `:root.theme-neutral`. **Nothing in Atlas ever applies those classes** — grep
`themesource/` and you will find no reference. They are a slot for you to drive.

Three consequences worth knowing before building any light/dark support:

1. **A token flip really does repaint Atlas.** Adding `theme-dark` to `<html>` on
   a running Mendix 11 app turns the page ground, cards, form controls, sidebar,
   buttons and DataGrid2 dark, with no per-widget CSS. This is materially better
   than Atlas 3, where the same trick left widgets light. And because the class
   is on `<html>`, popups and modals rendered at `<body>` follow it too.
2. **Your dark block must come after Mendix's.** `_theme-dark.scss` hardcodes
   stock Mendix blue at `:root.theme-dark`. Declare the same selector from a file
   imported later in `theme/web/main.scss` — same specificity, later wins — or
   your brand vanishes the moment the class appears.
3. **Never pin an Atlas variable to a literal colour.** Map it to a token
   (`--bg-color: var(--my-ground)`) so a variant restates the tokens, not the
   wiring. A hardcoded `--font-color-default` is invisible on a dark ground.

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 variants, or force `color: inherit` on those widgets.

For a working implementation of all of the above, read the generated
`theme/web/_mxcli-atlas-map.scss` in any themed project.

### Tokens stop at Atlas Core — the widget modules bake their colours

Re-pointing Atlas's custom properties covers the app, and then a few things stay
stubbornly off-palette: the Data Grid 2 pager caption, row-select checkboxes,
popover shadows. One cause: the theme source shipped by the **widget modules**
(`themesource/datawidgets`, `atlas_web_content`) styles some things with Sass
variables and literals. Sass resolves those at compile time, before any custom
property exists, so the value is baked into `theme.compiled.css` and **no token
can move it**. Only a later CSS rule can.

The worst case is `datawidgets/web/variables.scss:18`,
`$pagination-caption-color: #0a1325` — the "1–15 of 77" caption, which measured
**1.02:1** on a dark ground. The pager *buttons* beside it were fine, because
they resolve `var(--gray-darker, …)` through Atlas. Same bar, two mechanisms.

**The obvious fix does not work.** Each module's `main.scss` imports
`theme/web/custom-variables` *before* its own `!default` variables, so setting
`$pagination-caption-color: var(--my-muted)` there would win and Sass would
substitute the `var()` into every use site. Tempting, and wrong here:

1. The names collide with Atlas Core's, and Atlas Core feeds them to Sass colour
   functions — `atlas_core/web/_variables.scss:20` computes
   `mix($brand-primary, #e7e7e9, 10%)`. Handing `mix()` a `var()` is a compile
   error, so the app stops building.
2. The worst offenders are not behind a variable at all:
   `_three-state-checkbox.scss` writes `#264ae5` and `rgba(#264ae5, 0.4)`
   directly, so overriding `$brand-primary` would not reach them.

So it is a rule set, in a partial imported after the theme's own — see the
generated `theme/web/_mxcli-widgets.scss`.

**Read the compiled CSS, not the SCSS, when building one.** The sources are full
of `var(--token, #fallback)` declarations that already resolve correctly; only
the bare literals are a problem. In one measured app the stock blue `#264ae5`
appeared in 46 declarations — **24 of them harmless fallbacks**. Grepping the
source would have produced twice the rules for no benefit.

## CSS Hot-Reload Workflow

For theme/styling changes during Docker development:

```bash
# 1. Compile SCSS into deployment package (~55s)
mxcli docker build -p app.mpr

# 2. Push compiled CSS to browsers (instant, no page reload)
mxcli docker reload -p app.mpr --css
```

The `--css` flag calls the M2EE `update_styling` action, which pushes CSS via WebSocket to all connected browsers. **It does NOT compile SCSS** — always run `docker build` first.

For non-CSS changes (Class, Style, DesignProperties on widgets), use normal reload:
```bash
mxcli docker reload -p app.mpr
```

## Caveats

### DYNAMICTEXT + Style Crash

**Never** apply `style` directly to a DYNAMICTEXT widget — it crashes MxBuild with a NullReferenceException. Wrap in a CONTAINER:

```sql
-- WRONG: crashes MxBuild
dynamictext txt (content: 'Hello', style: 'color: red;')

-- CORRECT: style the container
container ctn (style: 'color: red;') {
  dynamictext txt (content: 'Hello')
}
```

This also applies to `alter styling` and `alter page set style` — never target a DYNAMICTEXT widget with Style.

### Clipped navigation labels are the CLOSED sidebar, not the theme

A sidebar item reading `All task` instead of `All tasks` is Atlas's **closed**
sidebar, which is an icon rail: `--navsidebar-width-closed: 48px`, set in Atlas's
own `themesource/atlas_core/web/themes/_theme-default.scss`. Measured against a
real compiled theme in a browser, the `<a>` for "All tasks" is **57px wide inside
a 48px rail** — the same overflow reported from a live app (56 in 48).

No mxcli theme sets any navigation *width*; the themes map colours only. So this
reproduces identically under `signal`, `ledger` and `console`, in both variants —
a layout constant, not a palette.

The fix is in the app, not the theme:

- **Give each nav item an icon.** That is what the closed rail is for; the icon is
  what stays visible when the sidebar is closed.
- **Or keep the sidebar open**, where the label has room.
- Shorter labels help, but only until the next one is too long.

Do **not** reach for `text-overflow: ellipsis` on the nav item as a blanket fix.
Tried and rejected: where Atlas does not also set `white-space: nowrap`, the label
wraps to two lines and reads fine — and the ellipsis rule turns that readable
`All / tasks` into `All / t…`. It trades one truncation for a worse one.

### DataGrid2 Renders ARIA `<div>`s, Not a `<table>` — and `Size` Is a Flex Weight

Two surprises when styling a **DataGrid2** matrix/pivot (ledger finding #46):

1. **It is not a `<table>`.** DataGrid2 emits `role="grid"` / `role="row"` /
   `role="gridcell"` **`<div>`s**, so `th, td { … }` selectors match nothing. A
   `td { white-space: nowrap }` intended to keep `€ 5,200` on one line does not
   apply, and amounts wrap. Target the ARIA roles instead:

   ```scss
   .ledger-matrix [role='gridcell'] { white-space: nowrap; }
   ```

   Playwright/tests see the same DOM: assert on `[role="row"]`, not `tr`.

2. **`Size` is a flex weight, not pixels.** On a column, `ColumnWidth: manual,
   Size: 132` does **not** set a 132px width — it divides available width by the
   weights across all columns. To give a wide matrix room, set a min-width on the
   grid and let it scroll:

   ```scss
   .ledger-matrix [role='grid'] { min-width: 1320px; }
   .ledger-matrix { overflow-x: auto; }
   ```

### Design Property Keys Are Case-Sensitive

Keys must match the `name` field in `design-properties.json` exactly:
```sql
-- CORRECT
designproperties: ('Spacing top': 'Large')

-- WRONG (case mismatch — silently ignored)
designproperties: ('spacing top': 'Large')
```

### Compound (Nested) Design Properties

Besides **flat** properties (a key with a single value — an option/dropdown
string or a toggle), `designproperties:` also supports **compound** properties:
one whose value is itself a set of sub-properties (e.g. Atlas's `Spacing` →
`margin-top`, `margin-bottom`, …). A compound value is written as a nested list:

```sql
designproperties: (
  'Column gap': 'Medium',                                       -- flat option
  'Cards style': ON,                                            -- flat toggle
  'Spacing': ('margin-top': 'Large', 'margin-bottom': 'Medium') -- compound
)
```

Supported on the **modelsdk** (`.mpr`) and **MCP** (live Studio Pro) backends.
Sub-property keys are case-sensitive, same as flat keys.

### ALTER STYLING Limitation with Builder-Created Pages

`alter styling` cannot find widgets in pages created by the MDL page builder because `walkPageWidgets` traverses `LayoutCall.Arguments` but the page parser doesn't fully reconstruct the widget tree when re-reading builder-created pages. These commands work on pages originally created in Studio Pro.

## Validation with `mxcli check -p`

When a project is supplied (`mxcli check page.mdl -p app.mpr`), design properties are
validated against the project's theme registry (`themesource/*/web/design-properties.json`):

- **MDL-WIDGET11** — a design-property key not defined for that widget type (with a
  case-sensitivity hint, or the list of valid keys).
- **MDL-WIDGET12** — an option value that isn't allowed; the message **lists the
  allowed values** (case-sensitive), which is the fastest way to fix a casing typo.

Both are warnings (a newer theme may add keys/values), so they inform without blocking.
`list design properties <widget>` lists the same allowed keys/values up front. On the
write side, the value's BSON type is taken from the registry (a `ColorPicker` /
`ToggleButtonGroup` property serializes as a custom value, not a plain option).

## Checklist

- [ ] Never apply `style` directly to DYNAMICTEXT — wrap in a CONTAINER
- [ ] Design property keys are case-sensitive — match `design-properties.json` exactly (`check -p` flags mismatches as MDL-WIDGET11/12)
- [ ] Compound/nested design properties (e.g. grouped Spacing) use a nested list: `'Spacing': ['margin-top': 'Large']`
- [ ] For CSS changes, run `docker build` then `docker reload --css`
- [ ] Use `describe styling` to verify changes after modification
- [ ] Check `docs/11-proposals/page-styling-support.md` for BSON format details

## Default styling + runtime theme switching (`mxcli theme list/show/create/apply/remove/switcher`, `mxcli new --theme`)

three embedded themes (**signal** light-first, **ledger** light-first, **console** dark-first), each a palette in `theme/web/custom-variables.scss` + a shared Atlas wiring partial + a theme partial imported from `theme/web/main.scss` (which compiles last), plus vendored fonts. **No model changes**, so it hot-applies under `run --local --watch` and cannot affect a build. Generated regions are digest-fenced: a block carrying local edits is refused rather than overwritten. Applying a theme removes the previous one. `--variant auto` (default) ships both palettes — the app follows `prefers-color-scheme` before first paint and honours a `theme-light`/`theme-dark` class on `<html>`; `light`/`dark` bakes one. `theme switcher install` is the only part that writes to the model (JS actions + a nanoflow for a toggle button). A project can add its own themes under `theme/mxcli-themes/<name>/` (committed, not compiled); `theme create <name> [--from <theme|design-file>]` scaffolds one from an existing theme, renaming the identifiers built from the name and optionally seeding the palette from `--mxt-*` declarations in a design artifact. A local theme shadows a built-in of the same name. Package: `cmd/mxcli/theme/`. See `docs/11-proposals/PROPOSAL_default_styling.md`

Attribution

mendixlabsmendixlabs
View sourceSee grades on GitHubMore from mendixlabs →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Terse caveman voice: answer first, fluff gone, every technical fact kept. Use for /caveman, "caveman mode", "talk like caveman", "be brief", "less tokens". Stays on until "stop caveman" or "normal mode".

1100021 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

698461 votes

Writing Skills

Create and manage Claude Code skills in HASH repository following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns), UserPromptSubmit hook, and the 500-line rule. Includes validation and debugging with SKILL_DEBUG. Examples include rust-error-stack, cargo-dep...

3931 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3421 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Amp, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Grok Build, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

741 votes
View all in ai-agents →