Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

TaskLessCustom properties
Keep the palette and sizesYes—
Calculate a shade (lighten, fade)Yes—
Switch the theme on the page—Yes
Change a value from JavaScript—Yes
Breakpoints in @mediaYes— (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.

Thanks for reading!

Donate to the site — we work hard for you.