Design tokens
The frontend design is driven by a LESS token system with central variables in custom.less. Tokens are exposed both as LESS variables (@colorPrimary) and as CSS custom properties (--color-primary) — the admin UI under /admin/design allows live editing without a rebuild. This page lists every token, the mapping to CSS variables, and the cache integration.
Token source
The central file lives not inside the theme but globally in the CMS:
_public/src/css/frontend/custom.lessIt's read by every theme during the frontend CSS build and compiled as part of the bundle. Editors can change its contents without a deploy directly in the admin under /admin/design — the changes land in the DB (website.custom_less) and are read from the DB on the next cache rebuild.
Colors (17 tokens)
| LESS variable | CSS variable | Default | Purpose |
|---|---|---|---|
@colorPrimary | --color-primary | #0368DB | Buttons, links, accents |
@colorPrimaryHover | --color-primary-hover | darken(#0368DB, 8%) | Button hover |
@colorDanger | --color-danger | #D40D12 | Errors, deletes |
@colorSuccess | --color-success | #45BF55 | Success |
@colorWarning | --color-warning | #FFC144 | Warning |
@colorText | --color-text | #333333 | Primary text color |
@colorTextLight | --color-text-light | #8A92A6 | Light text color, borders, inputs |
@colorWhite | --color-white | #ffffff | Backgrounds |
@colorBackground | --color-background | #ffffff | Page background |
@colorBorder | --color-border | #8A92A6 | Input borders, dividers |
@colorScrollbar | --color-scrollbar | #A3A3A5 | Scrollbar |
@colorOverlay | --color-overlay | rgba(0, 0, 0, 0.8) | Loader/modal overlay |
@colorMobileMenu | --color-mobile-menu | #014034 | Mobile menu background |
@colorCookieAccent | --color-cookie-accent | #11327a | Cookie consent accent |
@colorCookieText | --color-cookie-text | #58585a | Cookie consent text |
@colorLink | --color-link | #337ab7 | Alternate link color |
@colorError | --color-error | #ff0000 | Error border (inputs) |
Typography (8 tokens)
| LESS variable | CSS variable | Default | Purpose |
|---|---|---|---|
@fontBody | --font-body | 'Inter', sans-serif | Primary font |
@fontIcon | --font-icon | 'Font Awesome 6 Pro' | Icon font |
@fontSizeBase | --font-size-base | 14px | Base font size |
@fontSizeSmall | --font-size-small | 12px | Small text |
@fontWeightLight | --font-weight-light | 300 | Light weight |
@fontWeightRegular | --font-weight-regular | 400 | Regular |
@fontWeightMedium | --font-weight-medium | 500 | Medium/strong |
@lineHeightBase | --line-height-base | 1.429em | Line height |
Spacing (7 tokens)
| LESS variable | CSS variable | Default | Purpose |
|---|---|---|---|
@spacingBase | --spacing-base | 8px | Base unit |
@spacingSmall | --spacing-small | 10px | Small |
@spacingMedium | --spacing-medium | 20px | Medium |
@spacingLarge | --spacing-large | 30px | Large |
@spacingXLarge | --spacing-xlarge | 40px | Extra-large |
@spacingSection | --spacing-section | 40px | .ns-row margin-bottom |
@spacingForm | --spacing-form | 30px | .form-group margin-bottom |
Layout (5 tokens)
| LESS variable | CSS variable | Default | Purpose |
|---|---|---|---|
@radiusBase | --radius-base | 4px | Default border radius |
@radiusRound | --radius-round | 25px | Pill shape (round buttons) |
@inputHeight | --input-height | 44px | Height of <input>/<select> |
@buttonLineHeight | --button-line-height | 44px | Line height for .btn |
@scrollbarWidth | --scrollbar-width | 6px | Scrollbar width |
Breakpoints (2 tokens)
| LESS variable | Default | Use |
|---|---|---|
@breakpointDesktop | 991px | Tablet-to-mobile breakpoint for navigation, layout |
@breakpointMobile | 768px | Classic mobile cut |
Both breakpoints are only available as LESS variables — no CSS variables are generated (media queries don't accept CSS variables in the selector).
The Pagebuilder uses its own breakpoints
The tokens listed here (991px/768px) apply to the frontend theme layout (navigation, mobile menu). The Pagebuilder responsive system (column widths, spacing, flex) uses its own fixed set: 1200px / 992px / 768px for desktop/laptop/tablet/mobile. These Pagebuilder breakpoints are generated for media queries by cache_compress_css.php and are not editable via custom.less tokens — they're part of the Pagebuilder architecture.
Shadow (1 token)
| LESS variable | CSS variable | Default |
|---|---|---|
@shadowCard | --shadow-card | 0px 4px 20px rgba(0, 0, 0, 0.08) |
:root — CSS custom properties block
At the end of the token definitions, custom.less generates a :root {} block that exposes every token except breakpoints as a CSS custom property (breakpoints can't work this way, see the section below):
:root {
--color-primary: @colorPrimary;
--color-primary-hover: @colorPrimaryHover;
--color-text: @colorText;
--font-body: @fontBody;
--font-size-base: @fontSizeBase;
--spacing-base: @spacingBase;
--spacing-section: @spacingSection;
--radius-base: @radiusBase;
--shadow-card: @shadowCard;
/* ... and so on */
}Why both?
- LESS variables (
@colorPrimary): compile-time; usable in mixins and math (darken(),fade()). - CSS custom properties (
var(--color-primary)): runtime; can be changed dynamically via JavaScript (e.g. theme switch), and inherit through the DOM tree.
Components that only reference values can use either. Dynamic theme overrides go through CSS variables:
/* Dark theme override on body */
body.dark-theme {
--color-text: #f0f0f0;
--color-background: #0b0f1a;
}Menu editor token mapping
The menueditor plugin uses a second set of CSS variables with a --me-* prefix, derived from the design tokens:
.me-wrapper {
--me-bg: @colorBackground;
--me-primary: @colorPrimary;
--me-primary-fade: fade(@colorPrimary, 5%);
--me-text: @colorText;
--me-text-light: @colorTextLight;
--me-border: fade(@colorBorder, 25%);
--me-dropdown-bg: @colorBackground;
--me-dropdown-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
--me-spacing: @spacingBase;
--me-radius: @radiusBase;
--me-nav-height: 56px;
/* ... */
}This lets the menu editor carry its own deviations (e.g. me-dropdown-shadow is more specific than shadow-card) without touching the global tokens. The menu LESS lives at _public/src/css/frontend/menu.less and uses var(--me-*).
Details on the menu-editor system live in the php-backend-check skill in the "Menu Editor" section.
Live editing via /admin/design
The design-token editor at /admin/design shows the full contents of custom.less in a CodeMirror editor with LESS syntax highlighting and validation:
1. Open /admin/design
2. Tweak token values (e.g. @colorPrimary: #E11D48;)
3. Click "Save"
4. The core validates the LESS via Less_Parser
5. The new value lands in website.custom_less (DB column)
6. The core triggers a cache rebuild via rebuildCssCache()
7. The new tokens are active after the rebuild — no deployStep 6 usually runs automatically right after the save. When it fails (e.g. APCu permission, DB connection issue), the frontend keeps showing the old value — in that case, trigger /admin/cache → "Clear cache" manually. See Common issues below.
API actions (/api/backend/design)
| Action | Method | Purpose |
|---|---|---|
load_less | GET | Load custom LESS from the DB (fallback: file) |
save_less | POST | Validate LESS, store in the DB, rebuild cache |
reset_less | POST | Load the default from file and write it back to the DB |
After every save, the CSS cache is regenerated via rebuildCssCache() so the new token values flow into the production bundle.
Cache integration
The CSS build process (through the cache_settings plugin) loads custom LESS from the DB when present:
// cache_compress_css.php (excerpt)
function loadCustomLessFromDatabase() {
$q = query("SELECT custom_less FROM website LIMIT 1");
if (num_rows($q) > 0) {
$row = fetch_assoc($q);
return $row['custom_less'] ?? '';
}
return '';
}If the DB column is empty, the build falls back to _public/src/css/frontend/custom.less. That means:
- Fresh install: file tokens are active
- After the first save via
/admin/design: DB tokens override the file tokens (the file stays unchanged) - Reset via admin: clears the DB snapshot, file becomes the source again
Migration 013_add_custom_less_to_website.sql creates the column.
Components consuming tokens
Widgets and theme components reference the tokens directly:
// Inside the widget template or theme LESS
.my-widget {
background: @colorPrimary;
color: @colorWhite;
padding: @spacingMedium;
border-radius: @radiusBase;
font-family: @fontBody;
font-size: @fontSizeBase;
}Or through a CSS custom property (e.g. for dynamic switches):
.my-widget {
background: var(--color-primary);
color: var(--color-white);
padding: var(--spacing-medium);
}Common issues
Using a breakpoint as a CSS variable
@media (max-width: var(--breakpoint-mobile)) does not work — media queries don't accept CSS custom properties in the selector. Always use breakpoints as LESS variables: @media (max-width: @breakpointMobile).
Token changed in custom.less, the frontend still shows the old value
The cache rebuild is missing. Either click "Save" in the admin UI again (auto-triggers the rebuild) or hit /admin/cache → "Clear cache".
File changes to custom.less are ignored
As soon as the DB holds a value (website.custom_less != ''), the build ignores the file. Either use the "Reset" button in /admin/design (reloads the file default) or clear the DB column manually: UPDATE website SET custom_less = NULL.
LESS syntax error on save
The save_less endpoint validates via Less_Parser. Syntax errors come back with a line number and the save is rejected — the previous value stays active. For complex changes: test locally with the LESS compiler first.
AI-generated CSS overriding tokens
The AI CSS generator writes directly into custom_css fields per widget/row/column (not into the global tokens). If an AI response suggests "set primary color to red", it should not land as @colorPrimary: red; in widget CSS — that only overrides locally. For global token changes, always use the design editor.
See also
- Theme structure — where theme CSS lives and which
custom.lessfile applies - Environment variables — the
.envwithout token config - Widgets › Custom-CSS Convention —
.widget_{id}wrappers use the same tokens _public/src/css/frontend/custom.less— file source in the repo_public/extensions/core/backend/design/— the design plugin