5

I'm looking to support multiple themes in my app - moreover, I want to be able to dynamically change themes, either by changing a class on the body element, or even have different parts of the app use different themes.

In my previous project, I did it by adding explicit rules every time I need to use a theme-specific variable:

.theme-light & { background-color: @theme-light-background; }
.theme-dark & { background-color: @theme-dark-background; }

However, that approach does not scale well and adds unnecessary bloat to source files.

Now, I'm looking for a more automated approach for this. I.e. the following

.button {
  border-radius: 4px;
  background-color: @ui-background;
  color: @ui-foreground;
  border: 1px solid mix(@ui-background, @ui-foreground, 50%);
}

would turn into something like

.button {
  border-radius: 4px;
  border: 1px solid #808080;
    /* normally we wouldn't expect this to appear here, but in our case
    both themes have the same border color so we can't tell the difference */
}
.theme-light .button {
  background-color: #fff;
  color: #000;
}
.theme-dark .button {
  background-color: #000;
  color: #fff;
}

As far as I can tell, neither LESS nor SASS can do this in a natural way. It seems that it wouldn't be too difficult to implement it as a separate post-processor, that builds stylesheets for every theme, then compares them and scopes the differences into the corresponding "namespaces". I suspect that something like this might already exist, but I can't find anything.

Any suggestions?

riv
  • 6,846
  • 2
  • 34
  • 63
  • See https://stackoverflow.com/questions/25875846, https://stackoverflow.com/questions/23551080, https://stackoverflow.com/questions/32676764, etc. And please, don't add both Less and Sass tags for the same question - this does not make any sense. – seven-phases-max Dec 18 '17 at 18:29
  • 1
    Thanks for the links; why not add both? If I haven't decided which processor to use, if there was a convenient implementation of this feature for one of them, it could influence my decision. – riv Dec 18 '17 at 20:47
  • ecause it could only be one accepted answer. Same way you don't put fortran, c++, javascript, php, whatever else (notice all of them also can generate CSS) tag into an arbitrary Q. – seven-phases-max Dec 18 '17 at 21:59
  • Either way as a user I have a strictly opposite vision of your approach above. Imagine you have 18 themes, now (yet again as a user) assuming I will be really using *the only* theme for a page at once I won't very happy for my browser downloading one huge css containing all of 18 themes and then traversing through all of them when rendering. 18 themes -> 18 different css -> would be the most optimal/lightweight solution for most of use cases. – seven-phases-max Dec 18 '17 at 22:03

1 Answers1

7

Not sure about Less, but in Sass it can be implemented relatively easy by storing theme information into maps and using ability to pass content blocks into mixins using @content. Here is example of how it may look like, quite fast solution but you can get an idea:

// Themes definition
//  - First level keys are theme names (also used to construct theme class names)
//  - Second level keys are theme settings, can be referred as theme(key)
$themes: (
    light: (
        background: #fff,
        foreground: #000,
    ),
    dark: (
        background: #000,
        foreground: #fff,
    ),
);

// Internal variable, just ignore 
$_current-theme: null;

// Function to refer to theme setting by name
// 
// @param string $name  Name of the theme setting to use
// @return mixed
@function theme($name) {
    @if ($_current-theme == null) {
        @error "theme() function should only be used into code that is wrapped by 'theme' mixin";
    }
    @if (not map-has-key(map-get($themes, $_current-theme), $name)) {
        @warn "Unknown theme key '#{$name}' for theme '#{$_current-theme}'";
        @return null;
    }
    @return map-get(map-get($themes, $_current-theme), $name);
}

// Theming application mixin, themable piece of style should be wrapped by call to this mixin 
@mixin theme() {
    @each $theme in map-keys($themes) {
        $_current-theme: $theme !global;
        .theme-#{$theme} & {
            @content;
        }
    }
    $_current-theme: null !global;
}

.button {
    border-radius: 4px;
    @include theme() {
        background-color: theme(background);
        color: theme(foreground);
    }
}

This piece of code will give you this result:

.button {
  border-radius: 4px;
}
.theme-light .button {
  background-color: #fff;
  color: #000;
}
.theme-dark .button {
  background-color: #000;
  color: #fff;
}

Looks pretty close to what you're trying to achieve. You can play with this snippet at Sassmeister.

Flying
  • 4,422
  • 2
  • 17
  • 25
  • Ah thanks, didn't think of using functions inside mixin content. I might still try to look for a post-processing solution for better readability, though. – riv Dec 18 '17 at 20:51
  • this is awesome thanks, @riv any ideas for better readibility? I am working on a similar task – Ozan Mudul Jun 12 '23 at 08:01