Clientside theming with user generated values

  1. sass variables as fallback
  2. creating a theme map
  3. generating css custom properties from theme map
  4. crating a mixing to use theme values as css properties
  5. 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.