How to implement switchable themes in scss?
Asked Answered
P

3

18

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..?

Pentameter answered 16/10, 2020 at 13:58 Comment(3)
This will work until you decide to do say darken($background-color, 10%);Basketball
@YuryTarabanko ah.. I knew there had to be something :-/Pentameter
i think you can use RGB numbers instead of color names, that will fix this issueInterwork
G
13

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: black;

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.

Garmaise answered 16/10, 2020 at 14:4 Comment(6)
Would you recommend against defining and using the $background-color scss variable?Pentameter
@Pentameter I just updated my answer to show an example of how this could be done using SCSS. For theming, I would recommend SCSS variables as you can switch them on the go without having to style all elements that are customized by the theme.Garmaise
@Pentameter Depending on browser support, CSS variables may not work for you.Dasyure
@Dasyure thankfully that's not an issue (yes, I know I'm lucky ;-)Pentameter
@AmarSyla I just updated the question with details on how using css-variables as values for scss variables is compiled into the last code block in your answer. My question in the first comment was if you would recommend against mixing css-variables and scss variables in this way?Pentameter
@Pentameter I don't think that using an SCSS variable as a CSS variable would benefit you anything, since either way, you would be defining the variable only once in the code. If you prefer to use the same SCSS variable in multiple themes, you can do it, but in most cases, it doesn't provide any value in readability or functionality in my opinion.Garmaise
D
10

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 );
}
Dasyure answered 16/10, 2020 at 14:33 Comment(2)
It's certainly impressive :-) I'm not sure I'd call it light weight though, and it requires changing almost every rule in the existing (and very large) scss file...Pentameter
Why would you have to change your rules (assuming you mean CSS selectors)? You simply add @include themify {} where a theme style needs to be added and use the themed function with the property's value. The SCSS solution by @AmarSyla will require the same amount of work and the CSS variables solution would be the leanest, but I can see where one might not want ALL instances of --background-color to switch. In that case, you'll juggle more vars and have a similar overhead as the SCSS solutions. If a tiny function and a small mixin are not lightweight, then I don't know what to tell you.Dasyure
W
0

Add class="theme-light" to any root tag. Here we will toggle current theme with replacing class from "theme-light" to "theme-dark", for example.

<html class="theme-light">

Of course, "light" and "dark" you can replace with your theme name. Don't forget update $themes

Now we need to add two scss files. First,

themes.scss

$themes: (
  light: (
    primary: #fff,
  ),
  dark: (
    primary: #212121,
  ),
);

Second,

themed-style.scss

@import './themes';

@mixin themed-style($key, $color) {
  @each $theme-name, $theme-color in $themes {
    :global(.theme-#{$theme-name}) & {
      #{$key}: map-get(map-get($themes, $theme-name), $color);
    }
  }
}

Finally, use it

@import './themed-style';

.root {
  @include themed-style('background', 'primary');
}
Wilmington answered 17/11, 2023 at 7:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.