Skip to content

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.less

It'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 variableCSS variableDefaultPurpose
@colorPrimary--color-primary#0368DBButtons, links, accents
@colorPrimaryHover--color-primary-hoverdarken(#0368DB, 8%)Button hover
@colorDanger--color-danger#D40D12Errors, deletes
@colorSuccess--color-success#45BF55Success
@colorWarning--color-warning#FFC144Warning
@colorText--color-text#333333Primary text color
@colorTextLight--color-text-light#8A92A6Light text color, borders, inputs
@colorWhite--color-white#ffffffBackgrounds
@colorBackground--color-background#ffffffPage background
@colorBorder--color-border#8A92A6Input borders, dividers
@colorScrollbar--color-scrollbar#A3A3A5Scrollbar
@colorOverlay--color-overlayrgba(0, 0, 0, 0.8)Loader/modal overlay
@colorMobileMenu--color-mobile-menu#014034Mobile menu background
@colorCookieAccent--color-cookie-accent#11327aCookie consent accent
@colorCookieText--color-cookie-text#58585aCookie consent text
@colorLink--color-link#337ab7Alternate link color
@colorError--color-error#ff0000Error border (inputs)

Typography (8 tokens)

LESS variableCSS variableDefaultPurpose
@fontBody--font-body'Inter', sans-serifPrimary font
@fontIcon--font-icon'Font Awesome 6 Pro'Icon font
@fontSizeBase--font-size-base14pxBase font size
@fontSizeSmall--font-size-small12pxSmall text
@fontWeightLight--font-weight-light300Light weight
@fontWeightRegular--font-weight-regular400Regular
@fontWeightMedium--font-weight-medium500Medium/strong
@lineHeightBase--line-height-base1.429emLine height

Spacing (7 tokens)

LESS variableCSS variableDefaultPurpose
@spacingBase--spacing-base8pxBase unit
@spacingSmall--spacing-small10pxSmall
@spacingMedium--spacing-medium20pxMedium
@spacingLarge--spacing-large30pxLarge
@spacingXLarge--spacing-xlarge40pxExtra-large
@spacingSection--spacing-section40px.ns-row margin-bottom
@spacingForm--spacing-form30px.form-group margin-bottom

Layout (5 tokens)

LESS variableCSS variableDefaultPurpose
@radiusBase--radius-base4pxDefault border radius
@radiusRound--radius-round25pxPill shape (round buttons)
@inputHeight--input-height44pxHeight of <input>/<select>
@buttonLineHeight--button-line-height44pxLine height for .btn
@scrollbarWidth--scrollbar-width6pxScrollbar width

Breakpoints (2 tokens)

LESS variableDefaultUse
@breakpointDesktop991pxTablet-to-mobile breakpoint for navigation, layout
@breakpointMobile768pxClassic 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 variableCSS variableDefault
@shadowCard--shadow-card0px 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):

less
: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:

css
/* Dark theme override on body */
body.dark-theme {
    --color-text: #f0f0f0;
    --color-background: #0b0f1a;
}

The menueditor plugin uses a second set of CSS variables with a --me-* prefix, derived from the design tokens:

less
.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 deploy

Step 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)

ActionMethodPurpose
load_lessGETLoad custom LESS from the DB (fallback: file)
save_lessPOSTValidate LESS, store in the DB, rebuild cache
reset_lessPOSTLoad 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:

php
// 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:

less
// 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):

css
.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