2
votes

I have an existing project with a scss file that uses semantic variables:

$background-color: white;

body {
    background-color: $background-color;
}

I would like to change the background to black when I add a theming class to the body:

<body class="theme-dark">...</body>

and back to white if I remove the class (or switch to a theme-light).

I haven't found any light-weight methods to do this in scss (parametrizing a class for each theme seems like a very hard to maintain approach).

I've found a hybrid scss/css-custom properties solution:

original:

.theme-light {
    --background-color: white;
}

update (based on Amar's answer):

:root {
    --background-color: white;
}
.theme-dark {
    --background-color: black;
}

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

Defining the scss variable as having a css-variable expansion as the value, i.e. (from above):

$background-color: var(--background-color);

generates the following css:

:root { --background-color: white; }
.theme-dark { --background-color: black; }
body { background-color: var(--background-color); }

which seems to be what we want...?

I like it since it only requires changing the definition of $background-color (not every usage in a very large scss file), but I'm unsure if this is a reasonable solution? I'm pretty new to scss, so maybe I've missed some feature..?

2
This will work until you decide to do say darken($background-color, 10%);Yury Tarabanko
@YuryTarabanko ah.. I knew there had to be something :-/thebjorn

2 Answers

2
votes

Doing this with SCSS is possible but you would have to add styles to all elements you want to theme. That is because SCSS is compiled at build-time and you can't toggle the variables with classes. An example would be:

$background-color-white: white;
$background-color-black: white;

body {
  background-color: $background-color-white;
}

.something-else {
  background-color: $background-color-white;
}

// Dark theme
body.theme-dark {
  background-color: $background-color-black;

  .something-else {
    background-color: $background-color-black;
  }
}

The best way to currently do it is by using CSS variables. You would define the default variables like this:

:root {
  --background-color: white;
  --text-color: black;
}

.theme-dark {
  --background-color: black;
  --text-color: white;
}

Then, you would use these variables in your elements like this:

body {
  background-color: var(--background-color);
  color: var(--text-color);
}

If the body element has the theme-dark class, it will use the variables defined for that class. Otherwise, it will use the default root variables.

0
votes

All credit goes to Dmitry Borody

I would recommend an approach like what is mentioned in this Medium article. With this approach, you can assign what classes need to be themed without specifically mentioning the theme name so multiple themes can be applied at once.

First, you set up a SASS map containing your themes. The keys can be whatever makes sense to you, just make sure that each theme is using the same name for the same thing.

$themes: (
  light: (
    backgroundColor: #fff,
    textColor: #408bbd,
    buttonTextColor: #408bbd,
    buttonTextTransform: none,
    buttonTextHoverColor: #61b0e7,
    buttonColor: #fff,
    buttonBorder: 2px solid #fff,
  ),
  dark: (
    backgroundColor: #222,
    textColor: #ddd,
    buttonTextColor: #aaa,
    buttonTextTransform: uppercase,
    buttonTextHoverColor: #ddd,
    buttonColor: #333,
    buttonBorder: 1px solid #aaa,
  ),
);

Then use the mixin and function pair to add theme support.

body {
  background-color: white;
  @include themify {
    background-color: theme( 'backgroundColor' );
  }
}

.button {
  background-color: lightgray;
  color: black;

  @include themify {
    background-color: theme( 'buttonBackgrounColor' );
    color: theme( 'buttonTextColor' );
  }

  &:focus,
  &:hover {
    background-color: gray;
    
    @include themify {
      background-color: theme( 'buttonBackgroundHoverColor' );
      color: theme( 'buttonTextHoverColor' );
    }
  }
}

If you're going to be adding a lot of themes or a theme will be touching a lot of stuff, you might want to set up your SCSS files a little differently so that all the theming doesn't bloat your main CSS file (like the example above would do). One way to do this might be to create a themes.scss file and replicate any selector paths that need theming and have a second build script that outputs just the themes.scss file.

The Mixin

@mixin themify( $themes: $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;
    }

  }
}

The Function

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