Theming with SCSS: light and dark modes, optional brand skins, and CSS custom properties so the browser can switch without recompiling.
Start with semantic tokens
Theme tokens describe roles, not colours:
$theme-light: (
'bg': #ffffff,
'bg-subtle': #f5f5f5,
'text': #1a1a1a,
'text-muted': #5c5c5c,
'border': #d9d9d9,
'brand': #2563eb,
);
$theme-dark: (
'bg': #121212,
'bg-subtle': #1e1e1e,
'text': #f5f5f5,
'text-muted': #a3a3a3,
'border': #333333,
'brand': #60a5fa,
);
Emit CSS variables per theme
@mixin emit-theme($name, $tokens) {
@each $key, $value in $tokens {
--color-#{$key}: #{$value};
}
}
:root {
@include emit-theme('light', $theme-light);
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root {
@include emit-theme('dark', $theme-dark);
color-scheme: dark;
}
}
Components consume variables, not SCSS colour variables directly:
.card {
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
}
.button--primary {
background: var(--color-brand);
color: var(--color-bg);
}
Manual theme override
Let users pick a theme with a data attribute:
[data-theme='light'] {
@include emit-theme('light', $theme-light);
color-scheme: light;
}
[data-theme='dark'] {
@include emit-theme('dark', $theme-dark);
color-scheme: dark;
}
A small JavaScript toggle sets document.documentElement.dataset.theme. SCSS already compiled the rules; the browser swaps variable values.
Brand variants
Second brand? Add another map and scope it:
$theme-brand-b: (
'brand': #059669,
'bg-subtle': #ecfdf5,
);
.theme-brand-b {
@include emit-theme('brand-b', $theme-brand-b);
}
Wrap a section or the whole page in .theme-brand-b for a microsite or white-label preview.
What SCSS cannot do alone
Runtime theme switching needs custom properties in the output CSS. SCSS variables alone compile away – fine for build-time brand builds, not for a user toggle. Combine both: SCSS builds the variable blocks; the browser applies them.
Try it
- Define light and dark maps and emit variables on
:root. - Style a card and button with
var(--color-*)only. - Add
[data-theme='dark']override and test with DevTools.

