Less and CSS custom properties
Less variables exist only during compilation: the CSS keeps only the final values. CSS custom properties (--primary) live in the browser: they can change in a media query, for a single block or from JavaScript. A good scheme is to keep the values in Less and output them to the CSS as custom properties.
Outputting tokens to :root
Tokens are the basic design values: colours, spacing, radii. Keep them in a map and output them to :root with each():
// styles/tokens.less
@colors: {
primary: #2563eb;
surface: #ffffff;
text: #111827;
}
:root {
each(@colors, {
--color-@{key}: @value;
});
}Components then use the custom properties:
// styles/components/cards.less
.card {
padding: 16px;
color: var(--color-text);
background: var(--color-surface);
border-top: 4px solid var(--color-primary);
}// styles/main.less
@import "tokens";
@import "components/cards";:root {
--color-primary: #2563eb;
--color-surface: #ffffff;
--color-text: #111827;
}
.card {
padding: 16px;
color: var(--color-text);
background: var(--color-surface);
border-top: 4px solid var(--color-primary);
}A dark theme
Now the theme changes without recompiling: another set of values for the data-theme attribute is enough. The dark theme values can be calculated with Less functions:
// styles/themes/dark.less
@primary: #2563eb;
[data-theme="dark"] {
--color-primary: lighten(@primary, 15%);
--color-surface: #1f2937;
--color-text: #f9fafb;
}[data-theme="dark"] {
--color-primary: #6b95f1;
--color-surface: #1f2937;
--color-text: #f9fafb;
}<style>
:root {
--color-primary: #2563eb;
--color-surface: #ffffff;
--color-text: #111827;
}
.card {
padding: 16px;
color: var(--color-text);
background: var(--color-surface);
border-top: 4px solid var(--color-primary);
}
[data-theme="dark"] {
--color-primary: #6b95f1;
--color-surface: #1f2937;
--color-text: #f9fafb;
}
</style>
<div class="card">A light card</div>
<div data-theme="dark" style="margin-top: 8px">
<div class="card">A dark card</div>
</div>Import the file in main.less with @import "themes/dark";. The same .card markup looks different: inside data-theme="dark" other property values apply.
Who does what
| Task | Less | Custom properties |
|---|---|---|
| Keep the palette and sizes | Yes | — |
Calculate a shade (lighten, fade) | Yes | — |
| Switch the theme on the page | — | Yes |
| Change a value from JavaScript | — | Yes |
Breakpoints in @media | Yes | — (they do not work in media query conditions) |
Common mistake
Less colour functions run at compile time and do not know what var(--color-primary) will be in the browser:
// styles/components/buttons.less
.btn:hover {
background: darken(var(--color-primary), 10%);
}Compilation fails with the error Argument cannot be evaluated to a color. Calculate shades from a Less variable (darken(@primary, 10%)) and output the result to a separate property such as --color-primary-hover. If you need the shade in the browser, use the CSS color-mix() function.