htmlcsssassthemesscss-mixins

SCSS Theming with Dynamic Variables


Hi All!

I'm currently working on a theming feature for a CSS Framework and have run into some issues that I'm hoping you might be able to help out with.


I have created a SASS Map called $themes, which contains colors for different themes. I copy-pasted some code from a medium article(who doesn't) and boom my theming works! But...
This:

@include themify($themes) {
    .btn {
        color: themed('blue');
    }
}

on. every. component. is sloppier code than what I deem maintainable across the copious amounts of styling I'll be doing.
So...

My Goal


I'd like to do something super hacky and awesome like this:

@include themify($themes) {
    $blue: themed(blue);
}

I want to theme the variables so all I have to do is add $blue and not a lot of calling mixins and unnecessary mumbo jumbo.
If I could get something like this to work it would look something like this:

.btn {
    background: $blue;
}

All of the theming would be taken care of beforehand!
But of course it's never that easy because it doesn't work... It would be a godsend if one of you awesome sass magicians could pull some magic with this, I will include you in the source code of awesome contributors.

The Code


The $themes sass-map:

$themes: (
    light: (
        'blue': #0079FF
    ),
    dark: (
        'blue': #0A84FF
    )
);

The copypasta mixin from this awesome Medium Article:

@mixin themify($themes) {
  @each $theme, $map in $themes {
    .theme-#{$theme} {
      $theme-map: () !global;
      @each $key, $submap in $map {
        $value: map-get(map-get($themes, $theme), '#{$key}');
        $theme-map: map-merge($theme-map, ($key: $value)) !global;
      }
      @content;
      $theme-map: null !global;
    }
  }
}

@function themed($key) {
  @return map-get($theme-map, $key);
}

Any suggestions on how to accomplish this would be 100% appreciated. Appreciated enough to add you into the source code as an awesome contributor.

Thanks in Advance!


Solution

  • Sass does not allow you to create variables on the fly – why you need to manually declare the variables in the global scope.

    $themes: (
        light: (
            'text': dodgerblue,
            'back': whitesmoke 
        ),
        dark: (
            'text': white,
            'back': darkviolet
        )
    );
    
    
    @mixin themify($themes) {
      @each $theme, $map in $themes {
        .theme-#{$theme} {
          $theme-map: () !global;
          @each $key, $submap in $map {
            $value: map-get(map-get($themes, $theme), '#{$key}');
            $theme-map: map-merge($theme-map, ($key: $value)) !global;
          }
          @content;
          $theme-map: null !global;
        }
      }
    }
    
    @function themed($key) {
      @return map-get($theme-map, $key);
    }
    
    @mixin themed {
        @include themify($themes){
            $text: themed('text') !global;
            $back: themed('back') !global;      
            @content;
        }
    }
    
    @include themed {
        div {
            background: $back;
            color: $text; 
            border: 1px solid; 
        }
    }
    

    The problem about this approach (apart from being tedious to maintain) is that it will bloat your CSS with things that are not related to theming – in the example above border will be repeated.

    .theme-light div {
      background: whitesmoke;
      color: dodgerblue;
      border: 1px solid; //  <= 
    }
    
    .theme-dark div {
      background: darkviolet;
      color: white;
      border: 1px solid; // <=
    }
    

    While I think it is possible to create a setup that scopes each theme to it's own individual stylesheet (e.g. light.css and dark.css) I think you should consider using CSS variables to handle this

    $themes: (
        light: (
            'text': dodgerblue,
            'back': whitesmoke 
        ),
        dark: (
            'text': white,
            'back': darkviolet
        )
    );
    
    @each $name, $map in $themes {
        .theme-#{$name} {
            @each $key, $value in $map {
                --#{$key}: #{$value};
            }
        }
    } 
    
    div {
        background: var(--back);
        color: var(--text); 
        border: 1px solid;
    }
    

    CSS output

    .theme-light {
      --text: dodgerblue;
      --back: whitesmoke;
    }
    
    .theme-dark {
      --text: white;
      --back: darkviolet;
    }
    
    div {
      background: var(--back);
      color: var(--text);
      border: 1px solid;
    }
    

    Note! You only need to add the theme class to e.g. the body tag and the nested elements will inherit the values :)