Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Sass and custom properties for themes

Sass variables disappear after compiling, so they cannot switch a theme while the page is open. CSS custom properties can — but writing dozens of them by hand for every theme is tedious. The best of both: keep the themes in Sass maps and let a loop turn them into custom properties.

Themes in a map

Each theme is a map of token names to colours. Create styles/themes/_themes.scss:

// styles/themes/_themes.scss
$themes: (
  "light": (
    "bg": #ffffff,
    "text": #1f2937,
    "accent": #2563eb,
  ),
  "dark": (
    "bg": #111827,
    "text": #f9fafb,
    "accent": #60a5fa,
  ),
);

Generating custom properties

A mixin takes one theme and prints a custom property for every token. The --#{$name} interpolation builds the property name:

// styles/themes/_index.scss
@use "sass:map";
@use "themes";
 
@mixin theme-vars($theme) {
  @each $name, $value in $theme {
    --#{$name}: #{$value};
  }
}
 
:root {
  @include theme-vars(map.get(themes.$themes, "light"));
}
 
[data-theme="dark"] {
  @include theme-vars(map.get(themes.$themes, "dark"));
}

The light theme goes to :root — it is the default. The dark one applies when an element has the attribute data-theme="dark".

Components use var()

Components no longer know the colours: they read custom properties. These values are resolved in the browser, so they follow the current theme:

// styles/components/_cards.scss
.card {
  padding: 16px;
  color: var(--text);
  background: var(--bg);
  border: 2px solid var(--accent);
}
// styles/main.scss
@use "themes";
@use "components/cards";
:root {
  --bg: #ffffff;
  --text: #1f2937;
  --accent: #2563eb;
}
 
[data-theme=dark] {
  --bg: #111827;
  --text: #f9fafb;
  --accent: #60a5fa;
}
 
.card {
  padding: 16px;
  color: var(--text);
  background: var(--bg);
  border: 2px solid var(--accent);
}

Sass removed the quotes in [data-theme=dark] — for the browser it is the same selector.

Switching the theme

One attribute on <html> switches every colour. The script only sets the attribute; all colours are in the CSS:

<style>
  :root {
    --bg: #ffffff;
    --text: #1f2937;
    --accent: #2563eb;
  }
 
  [data-theme=dark] {
    --bg: #111827;
    --text: #f9fafb;
    --accent: #60a5fa;
  }
 
  .card {
    padding: 16px;
    color: var(--text);
    background: var(--bg);
    border: 2px solid var(--accent);
  }
</style>
 
<div class="card">
  <p>The card follows the theme.</p>
  <button type="button" id="toggle">Switch theme</button>
</div>
 
<script>
  document.querySelector('#toggle').addEventListener('click', () => {
    const root = document.documentElement;
    root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark';
  });
</script>

Click the button: the card's background, text and border change at once, without recompiling.

Who does what

TaskTool
Store themes and tokens, check namesSass maps and functions
Generate the variables for every themeSass @each
Switch a theme on the pageCSS custom properties
Read a colour in a componentvar(--accent)

Sass colour functions still help here: for example, a hover shade can be computed with color.adjust() for each theme while generating the map.

Common mistake

Sass functions cannot work with var(). color.adjust(var(--accent), $lightness: -10%) stops with an error: at compile time var(--accent) is just text, its colour is known only in the browser. Calculate shades in Sass before they become custom properties, or use CSS functions like color-mix() that work in the browser.

Thanks for reading!

Donate to the site — we work hard for you.