SCSS Theming with Dynamic Variables
Asked Answered
F

3

9

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!

Frictional answered 6/9, 2019 at 3:19 Comment(0)
Y
10

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 :)

Yeisk answered 6/9, 2019 at 8:19 Comment(2)
What do you mean by creating variables on the fly?Frictional
Say you have a map $map:( foo: 1, bar: 2 ) you will not be able to iterate over the keys and create $foo: 1; and $bar: 2; – you would have to create them manually like $foo: map-get($map, foo);Yeisk
F
2

I would use such approach (its looks a bit simplier):

$themes-names: (
    primary: (
        background: $white,
        color: $dark-grey,
        font-size: 18px,
    ),
    dark: (
        background: $dark,
        font-size: 10px,
    )
);
$primaryName: "primary";
$darkName: "dark";

@function map-deep-get($map, $keys...) {
    @each $key in $keys {
        $map: map-get($map, $key);
    }
    @return $map;
}

@mixin print($declarations) {
    @each $property, $value in $declarations {
        #{$property}: $value
    }
}

@mixin wrappedTheme($declarations) {
    @each $theme, $value in $declarations {
        $selector: "&.theme-#{$theme}";
        #{$selector} {
            @include print(map-deep-get($themes-names, $theme));
        }
    }
}



$color-default: map-deep-get($themes-names, "primary", "color");

.button-themed {
    /*extend some shared styles*/
    @extend %button;

    /* generate &.theme-primary{...} and &.theme-dark{...} */
    @include wrappedTheme($themes-names); 

    /*override*/
    &.theme-#{$darkName} {
        border: 5px solid $color-default;
        color: $white;
    }

    /* can override/extend specific theme  --modifier */
    &.theme-#{$primaryName}--modifier {
        @include print(map-deep-get($themes-names, $primaryName)); 
        /* will add all from: $themes-names[$primaryName]
        /---
            background: $white,
            color: $dark-grey,
            font-size: 18px,
        */
        font-size: 22px;
    }


    color: $color-default;
}
Fuze answered 5/2, 2020 at 11:0 Comment(1)
I tried to use this in Angular project and when I apply the styles and mixins at a global level it works, but when I do the same at the component level, it doesn't.Eckmann
D
0

For everyone still facing this problem: i started using a little workarround.

:root{
    --test: green;
}
    
$test: var(--test);
    
body{
    background-color: $test;
}

This resolves the scss variable as a css variable which you can change at runtime but still allows the scss syntax.. what will get lost if you do it like this is the ability to use the variable in something like scss's lighten() function. But i find this solution cleaner then the mixin hell and its easier to implement and maintain.

I do plead to the sass team to please provide a native solution for this.

Deathbed answered 20/3 at 15:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.