Appearance
Make a plugin work on desktop, tablet, and mobile
What you're doing
The platform is mobile-first: a page you author with no responsive settings renders as a single stacked column and already works on a phone. This guide is about the deliberate adjustments — hiding a column on small screens, a desktop-only side panel, a device-restricted menu item.
The default you get for free
- Rows and columns collapse to a single column below the
smbreakpoint. - A
core.gridbecomes horizontally scrollable inside its own container. - KPI rows wrap.
So the tutorial pages — header, KPI row, filter row, grid, dialogs — are usable on mobile with zero extra work. Verify that's true before adding overrides.
The breakpoint model
- The base breakpoint is
xs(mobile). It has no key insidebreakpointOverrides— its own hidden state is the sibling propertyhiddenAtXson the row / column / item. - Wider breakpoints are
sm,md,lg,xl, set underbreakpoints:
json
{
"id": "item-3",
"kind": "block",
"hiddenAtXs": true,
"breakpoints": {
"md": { "hidden": false },
"lg": { "span": 4 }
},
"block": { "...": "..." }
}Read that as: hidden on mobile, shown again from md up, and 4/12 columns wide from lg up. This is how you express "desktop-only content" — author it hidden at xs, re-show it at a wider breakpoint.
spanAtXs(on a column) is an independent 1–12 width for mobile, not a mirror ofsm.
The complete example — a detail panel that's a dialog on mobile, a column on desktop
The tutorial keeps it simple: detail is always a core.dialog, which works everywhere. If you want a side-by-side layout on desktop, put the detail content in a second column that is hiddenAtXs: true with breakpoints.lg.hidden: false, and keep the dialog for xs–md.
json
"rows": [
{ "id": "row-main", "columns": [
{ "id": "col-list", "items": [ /* grid */ ], "spanAtXs": 12, "breakpoints": { "lg": { "span": 7 } } },
{ "id": "col-detail", "hiddenAtXs": true, "breakpoints": { "lg": { "hidden": false, "span": 5 } },
"items": [ /* detail card bound to page.detailRecord */ ] }
] }
]Device-restricted menu items
A menu node's visibility.devices:
json
"visibility": { "visible": true, "enabled": true, "devices": ["desktop", "tablet"] }[] = every device. Use this for a bulk-admin screen you don't want on phones.
Page variants (advanced)
A page can carry variants — alternate layouts selected by role, device class, or a route param:
json
"variants": [
{ "id": "mobile", "rule": { "deviceClass": "mobile" }, "rows": [ /* a trimmed layout */ ] }
]Use this only when the mobile experience is genuinely a different screen, not just a reflow. For most list pages the default stacking plus a couple of hiddenAtXs flags is enough.
How to verify it worked
erp plugin test validates the breakpoint shape. For the real check, publish and open the page in the ERP, then use your browser's device-emulation to switch between phone, tablet, and desktop widths. Confirm:
- nothing overflows the viewport horizontally (the page body must never scroll sideways — only inner containers);
- every action reachable on desktop is reachable on mobile (even if in a different place);
- text stays legible (no fixed pixel widths forcing tiny text).
Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| page scrolls sideways on mobile | a fixed-width block or minWidth on a container | use relative units; let the grid scroll inside its own container |
xs override ignored | put an xs key inside breakpointOverrides | there is no xs key there — use hiddenAtXs / spanAtXs |
| desktop-only panel shows on mobile | set breakpoints.lg.hidden:false without hiddenAtXs:true | you must hide at xs first, then re-show |
| a dialog is unusable on a phone | fixed minWidth: 520px on the dialog content | use max-width: 100% / flexible widths |