Clientside theming with user generated values
- sass variables as fallback
- creating a theme map
- generating css custom properties from theme map
- crating a mixing to use theme values as css properties
- retrieving user data and applying user theme to current style
Sass variables as fallback
$background: #663399;
$background-darken-10: mix(black, $background, 10%);
$background-lighten-10: mix(white, $background, 10%);
$text: white;
I use sass' mix()
function instead of darken()
, because they behave diffrently in some cases. So I made this a habit. See this Pen by Josh McCarty.
Creating a theme map
I am using Sass maps to keep the theme organized and for later iteration when generating custom properties from it.
$theme: (
button: (
background: $background,
background-active: $background-darken-10,
background-hover: $background-lighten-10,
text: $text,
text-active: $text,
text-hover: $text,
)
);
Different background colors are obvious for the different states. But if you ask yourself, if it's necessary to repeat the text color for all states: I'll get to that one later on.
Generating css custom properties from theme map
The basic usage of custom properties (or css variables) is like this.
:root {
--custom-property: #FF0000;
--css-variable: #00FF00;
}
and is used like this.
.class {
property: value;
property: var(--custom-property, fallback);
}
The first declaration is for browsers that don't support custom properties, they will use this line, and will ignore the next one entirely.
:root {
@each $set, $value in $theme {
@each $key, $value in $value {
--#{$set}-#{$key}: #{$value};
}
}
}
generates the foloowing properties to our :root
.
:root {
--button-background: #663399;
--button-background-active: #5c2e8a;
--button-background-hover: #7547a3;
--button-text: white;
--button-text-active: white;
--button-text-hover: white;
}
Crating a mixing to use theme values as css properties
@mixin useTheme($theme, $set, $state: '') {
@if $state != '' {
$state: '-#{$state}';
}
@if map-has-key(map-get($theme, $set) , 'background#{$state}' ) {
background-color: map-get(map-get($theme, $set), 'background#{$state}');
background-color: var(--#{$set}-background#{$state}, map-get(map-get($theme, $set), 'background#{$state}'));
}
@if map-has-key(map-get($theme, $set) , 'text#{$state}' ) {
color: map-get(map-get($theme, $set), 'text#{$state}');
color: var(--#{$set}-text#{$state}, map-get(map-get($theme, $set), 'text#{$state}'));
}
}
.button {
@include useTheme($theme, 'button');
&:hover {
@include useTheme($theme, 'button', 'hover');
}
&:active {
@include useTheme($theme, 'button', 'active');
}
}
Retrieving user data and applying user theme to current style
An easy way to receive the user data is to get a json object. Something like this.
{
"--button-background": "#663399",
"--button-background-active": "#5c2e8a",
"--button-background-hover": "#7547a3",
"--button-text": "white",
"--button-text-active": "white",
"--button-text-hover": "white"
}
We fetch this JSON data from a URL and iterate over the variables to replace the once currently defined in the :root
scope.
function getThemingData() {
fetch("https://url.to/json")
.then(j => {
setCssVars(JSON.parse(j));
})
.catch(e => {
// error handling
});
}
function setCssVars(json) {
for (let key in json) {
document.documentElement.style.setProperty(key, json[key]);
}
}
Now that the css properties are replaced by the ones retrieved, the user theme is instantly applied the all the elements that use the corresponding var()
declarations.