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.
Consider the new restaurant 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.
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:
- Ignore the dependency created between a particularly theme and component
- 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.