Skip to content

Instantly share code, notes, and snippets.

@jrf0110
Last active April 23, 2021 09:05
Show Gist options
  • Save jrf0110/a431a63a64abb6ca516c to your computer and use it in GitHub Desktop.
Save jrf0110/a431a63a64abb6ca516c to your computer and use it in GitHub Desktop.

Theming in LESS

Following up to my "no global variables" in LESS post, here I describe how theming (or rather, micro-theming) can work.

All too often, I see developers attempt to fit an entire site design into a single theme (a set of variables that control the color scheme of the entire site). This results in a ton of non-semantic variables. Instead, we should build small themes (micro-themes), that expose a common interface to all consumers. Micro-themes are composed to create your application.

What constitutes a micro-theme?

Consider the new restaurant menu design:

Menu Design

Here, we have two distinct micro-themes: The default white theme, and the dark-gray theme.

Each one of these themes should expose a limited interface and should be accessible via mixins:

app.less

@import "./themes.less";

body {
  #themes > .get('default');
  background: @background;
  color: @text-color;
}

a {
  #themes > .get('default');
  color: @secondary;
}

...

// Your core build file can either wire up components to themes
// Or you could have the components themselves import the mixins.
// The latter introduces a dependency between a particular theme
// and a component, but there is a way around this which
// I will explain later.
.order-pane {
  // Or use the exposed mixins
  #themes > .get('dark-gray');
  .background();
  .color();

  a {
    color: @secondary-color;
  }

  // Accordion handle/header
  .order-pane-section-header {
    background: rgba( 0, 0, 0, 0.1 );
  }
}

themes.less

An example themes file from one of my projects

////////////
// Themes //
////////////
//
// Usage:
//    .something {
//      #themes > .get('neon-purple');
//      .background();
//    }
// 
// Properties:
//    @primary
//    @secondary
//    @tertiary
//    @text-color
//    @background
// 
// Mixins:
//    .background()
//    .color()

#themes {
  // Implement a theme locator mixin
  .get( @theme ) when ( @theme = 'neon-purple' ){ #themes > #neon-purple(); }
  .get( @theme ) when ( @theme = 'secondary' ){ #themes > #secondary(); }
  .get( @theme ) when ( @theme ){ #themes > #default(); }

  .standard( @text-color, @background ){
    .color(){
      color: @text-color;
    }

    .background(){
      background: @background;
    }
  }
  
  #default {
    @primary:       #1A57FF;
    @secondary:     #1A57FF;
    @tertiary:      #1A57FF;
    @text-color:    #333;
    @background:    #fff;

    #themes > .standard( @text-color, @background );
  }
  
  #secondary {
    @primary:       #1A57FF;
    @secondary:     #1A57FF;
    @tertiary:      #1A57FF;
    @text-color:    #fff;
    @background:    #aaa;

    #themes > .standard( @text-color, @background );
  }

  #neon-purple {
    @primary:       #B993D6;
    @secondary:     #8CA6DB;
    @tertiary:      #B993D6;
    @text-color:    #fff;
    @background:    @primary;

    #themes > .standard( @text-color, @background );

    .background(){
      background: @primary;
      background: linear-gradient( 90deg, @primary 10%, @secondary 90% );
    }
  }
}

As you can see, themes have no knowledge of the structure of the app or components; But rather, they describe the relationships between colors.

Implementing themes inside component definitions

It can become quite tedious to implement themes inside the build target file. It also does not allow for isolation of component knowledge.

There are two things we can do about this:

  1. Ignore the dependency created between a particularly theme and component
  2. Export a .theme(@theme-id) mixin on each component

The first of these options is self-explanatory, but I'll go in-depth on #2.

inputs.less

@import "../themes.less";

#components {
  #inputs {
    .init( @breakpoint: 500px, @theme ){
      input {
        max-width: 100%;
        border-style: solid;
        border-width: 3px;
        border-radius: 4px;
        background: transparent;

        .theme( @theme );
      }
    }

    // The theme mixin describes how a particular theme
    // applies to this component
    .theme( @name ){
      #themes > .get( @name );
      color: @text-color;
      border-color: fadeout( @text-color, 10% );

      &:focus {
        border-color: @text-color;
      }
    }
  }
}

Now in our main build file, we only need to specify what theme a component receives:

@import "./components/inputs";

#components > #inputs > .init( @theme: 'default' );

input.theme-dark {
  #components > #inputs > .theme('dark-gray');
}

This works quite well for components that only use one theme. However, what if a component implemented multiple themes? This could get ugly, specifying @primary-theme: 'default', @secondary-theme: 'dark-gray', ... and so on. At that point, it might be better to go with option #1.

If we decide to go with option #1, then we're deciding to accept that themes will be globally accessible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment