CSS Custom Property toggles for themes
The prefers-color-scheme CSS media feature provides a convenient method to change a website’s design based on the light or dark theme preference people set in their browser or operating system settings.
This setting is global though, so people can’t indicate a per-website preference. Because some people might want a dark system UI but light websites or the other way around, it is advisable to provide a theme selector on websites, too.
The repetition problem
Consider the following basic light theme:
:root {
--color-text: black;
--color-background: white;
color-scheme: light dark;
}
Now let’s add a dark theme by switching the properties:
@media (prefers-color-scheme: dark) {
:root {
--color-text: white;
--color-background: black;
}
}
For the site-specific theme switch (powerd by JS) we need even more CSS to override the media query:
:root.is-light {
--color-text: black;
--color-background: white;
color-scheme: light;
}
:root.is-dark {
--color-text: white;
--color-background: black;
color-scheme: dark;
}
You see why this gets repetitious really fast.
Extract property definitions
To avoid repeating definitions, we can set up “intermediate” variables that hold the properties for our two themes:
:root {
--color-text--light: black;
--color-text--dark: white;
--color-background--light: white;
--color-background--dark: black;
--color-text: var(--color-text--light);
--color-background: var(--color-background--light);
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root {
--color-text: var(--color-text--dark);
--color-background: var(--color-background--dark);
}
}
:root.is-light {
--color-text: var(--color-text--light);
--color-background: var(--color-background--dark);
color-scheme: light;
}
:root.is-dark {
--color-text: var(--color-text--dark);
--color-background: var(--color-background--dark);
color-scheme: dark;
}
But even this gets really long the more properties our themes have.
Custom Property toggles
Custom Property toggles “make” properties valid or invalid by prefixing them either with nothing or with the initial
keyword.
The code will be more readable later on if we first create “boolean type” variables:
:root {
--true: ;
--false: initial;
}
Now we can create our toggle variable --is-light-theme
:
:root {
--is-light-theme: var(--true);
}
@media (prefers-color-scheme: dark) {
:root {
--is-light-theme: var(--false);
}
}
:root.is-light {
--is-light-theme: var(--true);
}
:root.is-dark {
--is-light-theme: var(--false);
}
Then we set up our base variables, but this time we prefix all light ones with our --is-light-theme
toggle:
:root {
--color-text--light: var(--is-light-theme) black;
--color-text--dark: white;
--color-background--light: var(--is-light-theme) white;
--color-background--dark: black;
}
And finally we use the Custom Property fallback feature to toggle between light and dark theme properties.
This works because in light theme, all light properties are prefixed with nothing (see --true
variable) so they’re valid. In dark theme, all light properties are prefixed with initial
(see --false
variable) making them invalid.
The following var(…)
s take the light variable as the first parameter and the dark variable as the second parameter (fallback). If the light variable is valid it will be used, if it is invalid the second parameter (the dark variable) is used:
:root {
--color-text: var(--color-text--light, var(--color-text--dark));
--color-background: var(
--color-background--light,
var(--color-background--dark)
);
}
Final code
Finally we put all the code together and add the color-scheme
property so the browser automatically styles certain UI components:
:root {
--true: ;
--false: initial;
--is-light-theme: var(--true);
--color-text--light: var(--is-light-theme) black;
--color-text--dark: white;
--color-background--light: var(--is-light-theme) white;
--color-background--dark: black;
--color-text: var(--color-text--light, var(--color-text--dark));
--color-background: var(
--color-background--light,
var(--color-background--dark)
);
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root {
--is-light-theme: var(--false);
}
}
:root.is-light {
--is-light-theme: var(--true);
color-scheme: light;
}
:root.is-dark {
--is-light-theme: var(--false);
color-scheme: dark;
}