Migrate a Claude Design Prototype into a Mendix App (Theme + Pages)
SkillMediaLets your agent rebuild a design prototype inside a Mendix app, matching its colors and styles to the given design.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Migrate a Claude Design Prototype into a Mendix App (Theme + Pages) skill
About this capability
Reproduce a Claude Design prototype or design handoff (HTML/CSS, .dc.html export, tokens, screenshots) inside a Mendix app: build the palette with `mxcli theme create --from`, then apply classes in pages with MDL. Use when given a design artefact and asked to make the app look like it.
What this skill tells your AI
The instructions your AI receives, as published by mendixlabs/mxcli in .claude/skills/mendix/migrate-design-prototype/SKILL.md and read by ahel’s review.
When to Use This Skill
Use this skill when you are given a Claude Design prototype / design handoff (an
HTML/CSS prototype, a *.dc.html design-console export, a tokens file, a PRD, and/or
screenshots) and need to reproduce that look in a Mendix app using mxcli + MDL.
It covers the two halves of the job:
- Build the SCSS theme — turn the prototype's design language (colours, fonts,
spacing, component styles) into a Mendix theme in
theme/web/main.scss. - Apply it in pages — attach the theme's classes to widgets with MDL
(
Class:/DynamicClasses:oncreate page/alter page).
Related skills: atlas-design (read first — the Atlas-first taste + workflow layer),
theme-styling (SCSS compilation chain, hot-reload, styling caveats),
create-page (widget syntax), alter-page (in-place widget edits),
bulk-widget-updates (apply a class across many widgets).
The Pipeline at a Glance
Claude Design handoff Mendix app
───────────────────── ──────────────────────────────
*.dc.html / prototype ──① theme ──► mxcli theme create --from <file>
tokens / CSS / PRD create → a theme the project owns
│
component styles ──② rebuild ──► Atlas block / utility class, and only
(cards, chips, …) as classes then .ss-* classes in main.scss
│
screenshots ──③ reference ──► widgets get Class: / DynamicClasses:
(per screen) per screen via create page / alter page
│
──④ build ──► docker build → docker reload --css
│
──⑤ verify ──► compare running screen to screenshot
Golden rule: the prototype is the source of truth. Before building or polishing any screen, open the matching screenshot/handoff for that screen and match it — colours, spacing, font, component shapes. Do not invent styling the prototype doesn't show.
Atlas-first (read atlas-design). Reproduce the prototype with what Atlas already
gives you before hand-writing custom SCSS. In order of preference:
- An Atlas building block —
use building block Atlas_Web_Content.Card/Pageheader/List_Cardsetc. gives you the whole component's markup + styling for free. Discover withshow building blocks, inspect withdescribe building block. - Atlas utility classes and typed design properties —
class:'card',class:'btn btn-primary',spacing-inner-*/spacing-outer-*for padding/margin,flex-row/flex-column+align-x-*/align-y-*for layout (nolayoutgridneeded); or the typed equivalentsdesignproperties: ['Card style': on],['Background color': 'Brand Primary'],['Spacing': ['margin-bottom': 'L']].mxcli check -pvalidates design-property keys and values (MDL-WIDGET11/12) and lists the allowed values. - Brand-token retune — build the palette with
mxcli theme create --from(step ①). The theme maps ~60 Atlas variables onto it, so the whole app inherits the look; hand-mapping a handful of--brand-*leaves most of Atlas on stock blue. - Custom
.ss-*SCSS — for brand identity only. Reach for a hand-rolled component class (below) only when Atlas genuinely can't express the shape (bespoke chrome, fractional-track grids, pixel-exact rows). Hand-rolling.panel/.stat/.cardSCSS that just re-implements whatclass:'card'already does is the single most common mistake — seeatlas-design.
The rest of this skill (custom SCSS components, .ss-* classes, ListView row reshaping) is
layer 4 — the identity layer you drop to when the first three don't reach the design.
Where the Theme Lives (read this first — it avoids the main friction)
- Custom styles go in
theme/web/main.scssAFTER the@imports, or in your own partial. Styles placed after the imports win the cascade over Atlas defaults. Oncemain.scssgrows, prefer splitting a partial out for readability: createtheme/web/_<name>.scssand add@import "<name>";after the Atlas imports (the same cascade-order rule then applies within the partial). New partials are creatable — keep the import order (custom after Atlas) and everything works. - Use a project prefix for every custom class so it never collides with Atlas or
widget CSS —
.ss-panel,.ss-chip. Pick one and use it everywhere. Do not invent a parallel set of--ss-*colour variables: the theme's--mxt-*palette is already the app's vocabulary, and a second one silently stops following a theme or variant swap. theme/web/custom-variables.scssholds the palette, andmxcli theme applywrites it — it is a generated, digest-fenced block. Retune tokens there for a one-value tweak; for a real brand, own the theme (mxcli theme create, step ①) rather than editing inside the fence, which the nextapplyrefuses.theme/mxcli-themes/<name>/is where a theme the project owns lives. Committed, and not compiled — mxbuild's entry point istheme/web/main.scssand it does not globtheme/, so the sources sit inert untiltheme applycopies them intotheme/web/.- Do not hand-edit
theme-cache/web/— that is the compiled build artifact.
① Build the Theme — mxcli theme create --from
Do not hand-write a token block. A theme's palette is nothing but --mxt-*
custom properties, and mxcli builds one from a design artifact directly:
mxcli theme create acme -p app.mpr --from design/canvas.dc.html
mxcli theme apply acme -p app.mpr
--from reads --mxt-* declarations out of any CSS-shaped text — a stylesheet,
an SCSS partial, or the <style> blocks of an HTML export — wherever they appear:
:root { --mxt-brand: #2b5170; --mxt-ground: #eef1f4; --mxt-ink: #1a2129; }
@media (prefers-color-scheme: dark) { :root { --mxt-ground: #16161a; } }
Declarations inside a dark block (prefers-color-scheme: dark, .theme-dark,
[data-theme="dark"]) seed the dark palette; everything else seeds the light
one. Tokens the design does not name keep the base theme's value, so a
five-colour handoff still yields a complete, working palette.
Three reasons this beats writing the tokens yourself, and each is a mistake this skill used to teach:
- The Atlas mapping is already done, and it is ~60 variables, not eight.
Hand-mapping
--brand-primary,--topbar-bgand a handful of others leaves most of Atlas — form controls, tables, modals, the pluggable widgets — on stock Mendix blue. - Fonts are vendored, not
@imported from a CDN. A@import url("https://fonts.googleapis.com/…")inmain.scssis an@import-ordering trap, a third-party request on every page load, and it fails outright in an air-gapped deployment. The theme ships the.woff2files undertheme/web/mxcli-fonts/. - Light and dark come for free. A hand-written
:rootblock is one palette; every mxcli theme carries both and follows the OS before first paint.
What to extract from the handoff
Ask the design step to emit a --mxt-* block if you can — then this is a parse,
not a judgement call. Otherwise read the values off the prototype and write them
into a small tokens.css to pass to --from. Run mxcli theme show signal for
the full vocabulary; the ones that carry the look:
| From the handoff | Token |
|---|---|
| brand / primary, its hover, text on a brand fill | --mxt-brand, --mxt-brand-hover, --mxt-brand-ink |
| app background, cards/panels, striped rows, hover, selected | --mxt-ground, --mxt-surface, --mxt-surface-alt, --mxt-surface-hover, --mxt-surface-selected |
| body text, muted text, faint text, hairlines | --mxt-ink, --mxt-ink-muted, --mxt-ink-faint, --mxt-line |
| sidebar / topbar chrome and its text | --mxt-rail, --mxt-rail-line, --mxt-rail-ink, --mxt-rail-ink-active |
| ok / warning / danger / info | --mxt-success, --mxt-warning, --mxt-danger, --mxt-info |
| status chip fills and their text | --mxt-tint-ok / --mxt-tone-ok (and -warn, -risk, -info, -neutral) |
| body font, headings, mono, base size, line height | --mxt-font, --mxt-font-heading, --mxt-font-mono, --mxt-font-size, --mxt-line-height |
| corner radius, row/control height, elevation, focus ring | --mxt-radius, --mxt-radius-lg, --mxt-row-height, --mxt-control-height, --mxt-shadow, --mxt-focus-halo |
A --mxt-* name the base theme does not declare is refused, not written.
Nothing reads it, so the theme would apply cleanly and render unchanged — which
is indistinguishable from the design never having been applied. If a handoff
value has no token, it belongs in the theme's own skin (below), not in the
palette.
Two constraints worth knowing before you start
- The navigation rail stays dark in both palettes. Several Atlas topbar
widgets paint their own text assuming a dark rail, at a specificity a simple
override cannot beat. If the prototype has a light sidebar, expect to fight it
— see
theme-styling. - Never pin an Atlas variable to a literal colour. Map it to a token so the
dark variant restates ~30 values instead of ~60. A hardcoded
--font-color-defaultis invisible the moment the ground goes dark.
Fonts the theme does not ship
The built-in themes vendor IBM Plex, Source Sans/Serif, JetBrains Mono and Space
Grotesk. For a different family, drop the .woff2 files into
theme/mxcli-themes/<name>/files/theme/web/mxcli-fonts/, add an @font-face
loop to that theme's partial beside the existing ones, and point --mxt-font at
it. Vendored, for the reasons above — not a CDN @import.
If the design needs more than the palette
Genuinely bespoke chrome goes in the theme's own skin mixin
(@mixin mxcli-<name>-skin in theme/mxcli-themes/<name>/files/theme/web/_mxcli-<name>.scss),
where it is scoped with the theme and survives theme apply. Reach for .ss-*
classes in main.scss only for per-screen identity that is not part of the
design language — and read atlas-design first, because most of what looks
bespoke is an Atlas building block or utility class.
② Rebuild Components — Atlas block/class first, custom class only for identity
For each repeated element in the prototype (panel, stat tile, chip, card, table row,
progress bar…), first check whether Atlas already provides it (Atlas-first, above):
is there a building block (show building blocks) or an Atlas class / design property
(card, btn-*, spacing-*, flex-*+align-*, ['Card style': on]) that gets you
most of the way? If so, use it and add a thin .ss-* class only for the brand delta
(colour, radius, font). Re-implementing card/panel/btn from scratch is the mistake
atlas-design exists to prevent.
When Atlas can't express the shape, write one reusable class driven by the theme's
tokens. Never a literal colour: a literal survives a light/dark flip and a theme
swap, and is wrong under every palette but the one you wrote it against. Keep classes
small and composable so a widget can stack several (Class: 'ss-panel ss-grid-lv').
Check the theme first — .pill and .stat below already ship as recipe classes
(mxcli theme show <name>), so a chip and a KPI tile usually need no CSS at all.
// Surface panel — every value resolves through the palette
.ss-panel {
background: var(--mxt-surface);
border: 1px solid var(--mxt-line);
border-radius: var(--mxt-radius);
box-shadow: var(--mxt-shadow);
}
// Status chip — one base + colour modifiers. The theme's own `pill` /
// `pill-ok` / `pill-warn` / `pill-risk` do this already; write your own only
// if the design's shape genuinely differs.
.ss-chip {
display: inline-block; border-radius: 11px; padding: 2px 10px;
font-family: var(--mxt-font-mono); font-size: 11px; font-weight: 600;
border: 1px solid transparent;
white-space: nowrap; // status chips must never wrap to 2 lines
}
.ss-chip--ok { background: var(--mxt-tint-ok); color: var(--mxt-tone-ok); }
.ss-chip--danger { background: var(--mxt-tint-risk); color: var(--mxt-tone-risk); }
Base + modifier convention. Give each component a base class and add --variant
modifiers for state/colour (.ss-chip + .ss-chip--danger, .ss-heat--ok/--warn/--over).
Widgets then combine base + modifier: Class: 'ss-chip ss-chip--danger'.
Reshaping Mendix chrome. To make Atlas widgets read like the prototype you often need to override Mendix's own DOM classes. Common targets:
-
Sidebar / topbar shell:
.region-topbar,.mx-header,.region-sidebar,.mx-scrollcontainer-left, and nav items under.mx-navigationtree. -
ListView rows are the workhorse for grids/tables — neutralise Atlas's default row chrome (padding/border/background) so rows read as your design's grid lines:
.ss-grid-lv > ul > li, .ss-grid-lv .mx-listview-item { padding: 12px 16px !important; margin: 0 !important; border-bottom: 1px solid var(--mxt-line); } -
::before/::afteron.mx-navigationtreecan inject brand blocks / section labels the design shows but the Mendix nav model doesn't produce.
Use !important sparingly but expect to need it when overriding Atlas widget CSS.
Component → Mendix widget map
The lookup that removes the guesswork: for each component in the prototype, pick the widget
here first, then style it with your --<prefix> classes. Validated across the BAE Resource
Scheduling and Expense Approval designs.
| Design component | Mendix widget | Notes |
|---|---|---|
| Page / screen canvas | container | one per page, e.g. Class: 'ea-page' |
| Card / panel / section | Atlas Card building block, or container class:'card' / ['Card style': on] | drop to a custom panel class only for brand delta |
| KPI / stat tile | container | label + value + delta as child dynamictext |
| Row/column layout (even columns, gaps) | container with flex-row/flex-column + align-* (or ['Flex container': …]) | no layoutgrid needed for simple flex layouts |
| Multi-column / dashboard layout | layoutgrid + row + column | for exact fractional tracks (2.4fr 1.2fr …) use a container styled display:grid instead — see Layout techniques |
| Heading / title | dynamictext (RenderMode H1/H2) | |
| Body / label / caption / table cell | dynamictext | the workhorse — text is inline, see techniques |
| Metric / big number | dynamictext | mono class |
| Chip / badge / tag / status pill | dynamictext | base class + colour modifier; leading dot via CSS ::before |
| Data table / grid / row list | listview (database source; row = layoutgrid or grid container) | preferred for bespoke row layouts — full control of the row markup; the datagrid pluggable widget exists but is heavier to style to a custom design |
| Table header row | static header band (container/layoutgrid) above the listview | |
| Tabs / segmented / filter-chip row | tabcontainer styled as pills (one tabpage per XPath-filtered view) | for static/decorative chips use dynamictext |
| Master list + detail pane | listview (Selection) + dataview (DataSource: SELECTION) | |
| Detail / read view | dataview | |
| Create / edit form | dataview + inputs + footer | Save/Cancel in the footer |
| Text input / multiline | textbox / textarea | |
| Dropdown / enum select | combobox | bound to an enum or association |
| Date field | datepicker | |
| Boolean / toggle | checkbox | |
| Button (primary/secondary) | actionbutton | ButtonStyle or a class |
| Link / text button | linkbutton | |
| Search box | listview built-in search bar | hoist/restyle via CSS |
| Avatar / initials | dynamictext | styled as a circle |
| Image / logo / thumbnail | image / dynamicimage / staticimage | |
| Icon / colour dot | CSS ::before on a class | |
| Chart (line/bar/column/pie/area/bubble) | chart pluggable widget (Mendix Charts / ChartJS) | via PLUGGABLEWIDGET '<id>' — see Pluggable widgets below; needs a datasource + series config |
| Donut / gauge | ProgressCircle pluggable widget | via PLUGGABLEWIDGET '<id>'; static or attribute-driven — worked example below |
| Progress bar / meter | progressbar widget, or a container (track + fill) | a styled track+fill container needs no widget package |
| Sparkline / bespoke SVG | HTMLElement pluggable widget, or a container with a CSS SVG background | embed the design's inline SVG directly |
Layout techniques
- Exact fractional columns. Atlas's
layoutgridis a 12-column system and can't express ratios like2.4fr 1.2fr 1fr 1.4fr 0.6fr. For pixel-faithful tables/dashboards, style a plaincontainerasdisplay:grid; grid-template-columns: …in its class and put the cell widgets as its direct children — each widget becomes a grid item. dynamictextis inline by default. For stacked text (a title over a subtitle) setdisplay:blockin the class, or the lines run together.
Pluggable widgets (charts, donut, HTML/SVG)
Pluggable widgets do round-trip through MDL — but not by bare name. A bare
progresscircle / CUSTOMWIDGET is rejected by the builder ("unsupported widget
type"). The working form uses the widget's full package id as a quoted string:
PLUGGABLEWIDGET '<widget.package.id>' widgetName ( prop: value, … ) { childslots }
One-time registration. The widget package must be present in the project's widgets/
before you can reference its id:
mxcli widget init -p baedemo.mpr # scaffold pluggable-widget support (run once)
mxcli widget extract -p baedemo.mpr --mpk widgets/ProgressCircle.mpk # register a package
mxcli widget list -p baedemo.mpr # list available widget ids + their props
mxcli widget list prints each widget's id and property names — copy the id verbatim into
the PLUGGABLEWIDGET '…' string, and use the property names it reports as the widget's props.
Worked example — the status donut (Expense Dashboard). A ProgressCircle in static mode,
with a text label overlaid via a sibling container (the widget draws only the ring):
container donutWrap (Class: 'ea-donut') {
PLUGGABLEWIDGET 'com.mendix.widget.custom.progresscircle.ProgressCircle' donut (
type: 'static', staticCurrentValue: 67, staticMinValue: 0, staticMaxValue: 100, showLabel: false
) { }
container donutLabel (Class: 'ea-donut-label') {
dynamictext donutPct (Content: '67%', Class: 'ea-donut-pct')
}
}
This passed mx check with 0 errors, survived docker build, and renders its SVG arc at
runtime (verified on the dashboard).
Real Mendix Charts (BarChart/LineChart/PieChart/HeatMap/…) are fully authorable
too — each series/line binds its own OQL-view datasource + X/Y attributes;
Pie/HeatMap bind at the widget level (ValueAttribute:, Pie needs SeriesName:).
See Custom & Pluggable Widgets → Charts for the chart-type
→ id table, per-chart required-property gotchas (TimeSeries needs a datetime X,
Bubble needs a size attribute), and the CE0463 → mxcli docker check/build step
(these normalize widgets and preserve MPRv2 storage — never run bare
mx update-widgets on a mxcli new project; it deletes mprcontents/).
mdl-examples/doctype-tests/34-chart-widget-examples.mdl is the full showcase.
Pluggable-widget gotchas:
- Reach for built-ins first.
listview,dynamictext,container,gallery,comboboxneed no registration. Drop to a pluggable widget only when the design genuinely needs one (charts, gauges, embedded SVG, maps, sliders). Many "charts" in a handoff are just static SVG — acontainerwith a CSSbackgroundSVG (KPI sparklines, area trends here) is lighter than a real chart widget and needs no datasource. - Reserved keywords can't be widget names —
activity,legend, etc. are rejected by the parser; rename (actCard,legendCol). - Empty slot is
{ }. Always close the child-slot braces, even when empty.
The App Shell: Navigation & Layout (built once, not per page)
Most Claude Design prototypes render a persistent sidebar + topbar on every screen — a brand block, a menu, sometimes a footer tag. It is tempting to rebuild that chrome inside each page. Don't. In Mendix the shell is not a page — it comes from two shared places:
- The layout (
Atlas_Core.Atlas_Defaultin this project) provides the topbar + left sidebar regions. Every page setsLayout: Atlas_Core.Atlas_Default, so they all inherit the same shell; the page's own widgets render only in the content region. - The navigation profile supplies the menu items. One
Responsiveprofile drives the whole app — home page, login page, and the flat/nested menu. Menu items point at pages, not at widgets you place.
So the prototype's sidebar maps to navigation config + layout styling, configured once, and its menu grows by adding navigation items — never by editing pages.
Add a screen to the menu
mxcli -p baedemo.mpr -c "SHOW NAVIGATION" # profiles, home page, item count
mxcli -p baedemo.mpr -c "SHOW NAVIGATION MENU Responsive" # the menu tree
Add or reorder items with CREATE OR REPLACE NAVIGATION <Profile> … (full-replacement — dump
the current profile first with DESCRIBE NAVIGATION <Profile>, edit, re-apply). See
manage-navigation for the item syntax, home/login pages, and role-based homes.
Style the shell to match the design
The menu items and regions are standard Atlas DOM, so the prototype's look is reproduced with
CSS in main.scss (step ②) — you do not model the sidebar's chrome as widgets:
- Recolour the regions via the mapped Atlas vars (
--sidebar-bg,--topbar-bg,--navigation-bg) or by overriding.region-sidebar/.region-topbar/.mx-headerdirectly. - Restyle menu entries under
.mx-navigationtree(idle / hover / active states, spacing, the active-item accent bar). - Inject chrome the nav model can't express — a brand logo block, a
WORKSPACEsection label, anITERATION 1 · DEMOfooter tag — with::before/::afteron.mx-navigationtree(or the sidebar region). The Mendix navigation model has no field for these, so CSS pseudo-elements are the right tool; keep their text in the SCSS with the rest of the theme.
Rule of thumb: if a design element is the same on every screen, it belongs to the shell (navigation + layout + CSS), not to a page. Only the content region is built per-page in ③.
Restructuring the shell to match the prototype (full-height sidebar, fixed topbar)
Recolouring is rarely enough — most prototypes put a full-height sidebar (brand block at the
very top) with the topbar only over the content, whereas Atlas_Default renders the topbar
full-width above a sidebar+content row. Reproduce the prototype layout with CSS, no custom
layout document needed:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 122
- Forks
- 49
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
migrate-design-prototype- Source
- github.com/mendixlabs/mxcli