Ember Theming

Theming a web application typically involves swapping out stylesheets or moving CSS classes around. But what if you have a single page application? It’s a little tricky, but EmberJS and SASS make it incredibly easy.

Goals

There are two primary goals that I wanted to achieve:

Swap Themes Quickly

Rather than have code change colors, fonts, etc. on the fly, it makes a lot more sense to already have those styles pre-defined in CSS.

Be Extensible

When exposed as an Ember Addon, our implementation should allow for 3rd parties to add their own themes easily.

How It Works

This implementation assumes a few things:

The bulk of the theme-switching work is done by a theme service and some SASS mixins.

Let’s Get SASSy

Let’s first look at the SASS code. There’s a $themes map declared. It’s written so that there are base themes and themes within the base. For example, suppose you had different areas of your application to theme, but they should still be a part of your overall theme (like a namespace):

In this example, foo is the base theme and primary and secondary are the sub-themes.

The properties of the map are arbitrary, but they are what you’ll refer to later on in CSS, sort of like a theme API. Think of it like “for this div.class-name, use the primary color for the background”.

Note: We will be using a data-theme attribute on HTML tags. I thought this was cleaner than swapping class attributes and the intent is clear in the markup.

Here is a gist of the SASS functionality to make theme switching happen. I’ll come back to using it momentarily.

// theme map
$themes: (
default: (
first: (
primary: #58b15f,
secondary: #e3f0d8
),
second: (
primary: #287f6e,
secondary: #83e5d2
)
)
);
// if theme service exists as addon - allow 3rd parties to merge into themes.
@if variable-exists(theme-additional) {
$themes: map-merge($themes, $theme-additional);
}
@mixin apply-theme() {
@each $base, $attributes in $themes {
@each $section, $values in $attributes {
$name: "#{$base}-#{$section}";
&[data-theme="#{$name}"] {
}
}
}
}
// helper for SASS files to apply theme values to an element.
// usage: @include theme('color', 'primary');
@mixin theme($cssAttribute, $themeValue) {
@each $base, $attributes in $themes {
@each $section, $values in $attributes {
$name: "#{$base}-#{$section}";
&[data-theme="#{$name}"] {
@if $cssAttribute == "background" and $themeValue == "bg-image" {
$url: map-get($values, $themeValue);
#{$cssAttribute}: url($url) repeat-x;
} @else if $cssAttribute == "background" and $themeValue == "icon" {
$url: map-get($values, $themeValue);
#{$cssAttribute}: url($url) no-repeat;
} @else {
#{$cssAttribute}: map-get($values, $themeValue) !important;
}
// ...
}
}
}
}
// helper for more advanced theming.
// usage: @include theme-advanced('border', '', 'accent', '1px solid');
@mixin theme-advanced($cssAttribute, $before, $themeValue, $after) {
@each $base, $attributes in $themes {
@each $section, $values in $attributes {
$name: "#{$base}-#{$section}";
&[data-theme="#{$name}"] {
#{$cssAttribute}: #{$before} map-get($values, $themeValue) #{$after};
}
}
}
}
view raw themes.scss hosted with ❤ by GitHub

Theme Service

Now let’s take a look at the theme service. Anything that’s going to consume this service will be able to:

import Ember from 'ember';
export default Ember.Service.extend({
base: 'default',
theme: 'first',
// the property used as a reference for styles
name: Ember.computed('base', function() {
const base = this.get('base');
const theme = this.get('theme');
return `${base}-${theme}`;
}),
// update things that may be using data-theme
themeChanged: Ember.observer('base', 'theme', function() {
this.notifyPropertyChange('name');
}),
// set the base theme for the application
setBase: function(base) {
this.set('base', Ember.isEmpty(base) ? 'default' : base);
},
// set theme to use within base theme
setTheme: function(theme) {
this.set('theme', Ember.isEmpty(theme) ? 'first' : theme);
}
});

Theming Your Application

How you determine when themes change is up to you. But here’s an example of swapping themes at a route:

import Ember from 'ember';
export default Ember.Route.extend({
theme: Ember.inject.service(),
// set theme to "second" when hitting route
beforeModel: function() {
this._super(...arguments);
this.get('theme').setTheme('second');
},
actions: {
// set theme to "first" when leaving route
willTransition: function() {
this.get('theme').setTheme('first');
this._super(...arguments);
}
}
});

Determining what you want to theme is also up to you. Let’s assume you have a component, here’s an example of using the theme service:

import Ember from 'ember';
export default Ember.Component.extend({
theme: Ember.inject.service()
// ...
});
<div class="my-awesome-class" data-theme="{{theme.name}}">
stuff goes here
</div>

Two things to note:

As the theme.name property changes, your data-theme will as well.

Last, to wire this all together, you have to style your div with the theme property you want. For example:

// usage:
div.my-awesome-class {
@include theme('color', 'primary');
}
// results in the output:
div.my-awesome-class[data-theme="default-first"] {
color: #58b15f;
}
div.my-awesome-class[data-theme="default-second"] {
color: #287f6e;
}

Only the first couple lines are relevant, the remainder displays what the resulting CSS will look like.

Extending

In the SASS code, it merges a property $theme-additional if it exists into the $themes map. If this code were available as an addon, you could add your own themes as follows:

Wrapping Up

If you thought this was interesting, found a way to make it better or wound up doing something awesome with it, I’d love to hear about it.

Here’s an example of theme switching in action:

Ember Theming

You can view the full gist [here](https://gist.github.com/jonpitch 655194358902cf3a0c05647dc1aca6a0).