A Sassy Approach To Theming Components
At Ticket Arena I have been helping to create a unified UI Library using React and while the component based approach is brilliant for building websites, it sometimes raises difficult questions about how best to customise a component for a specific usage. These questions also frequently include understanding how to best provide variations in our CSS to cover using components in different contexts.
Take the example of a button and a panel.
In our library, a panel is typically transparent with the standard body font colouring, but sometimes we use our brand colours as the background with white text on top. These two styles are assigned through classes - either .panel
for the default colours or .panel .panel--brand
for the brand colours.
Now, we also have a button class which has several modifiers whose styling show the intended purpose and outcome of clicking/tapping the button, these are .btn--primary
, .btn--secondary
and .btn--tertiary
where .btn--primary
returns a button that has white text on our brand blue background.
In addition to changing the colour schemes, our button classes also highlight the perceived importance of the component through font weight, font size and various hover effects.
Our designer wants to create a new area on our site which features a .panel--brand
and contains a .btn--primary
and this is where we hit our problem, how do we create a suitable alternative for our primary button?
A solution using BEM
Following the principle of BEM naming, it’s possible to solve this problem by creating a new modifier class for the button, maybe .btn--primary--light
. This works but what happens when we expand the panel to contain another component which previously had the blue background? We’ll keep creating context specific classes across many different components (which may or may not share the same modifier name) and we’ll end up with increasingly complex class names.
The emphasis falls on the developer to keep naming consistent and provide all the required variations. This spreads the theming aspect of our Sass across all our files, but not in a good way - there’s no way to determine exactly which themes are available as a whole and often the same colour schemes are used in different component modifier classes.
The colours you use are not restricted to the type of component
Instead of continually adding BEM named variants to our components, what if we looked at how colour schemes are used across all components? A blue background and white text needn’t be restricted to a .button--primary
or .panel--brand
, it can be applied to any component when a designer deems it appropriate.
Rather than using BEM to create individual component variations, I’m suggesting named themes which are applied to components separately to their main styling declarations.
We should find a way to automatically create one class per theme for our components where the selector follows a strict naming convention. I suggest using the following: .{component}-t-{theme-name}
, where the t
is there to show that this is the class that defines the theme properties.
Now our implementation of colour is consistent throughout our Sass code (no more .panel--brand
and .btn--primary
confusingly sharing the same colour scheme) so knowing what name to use to set a component’s colour is trivial. Also, anyone inspecting our source code can quickly understand which theme is being used on a component so new team members have a lower barrier to entry when working with the front-end code.
Lastly, a centralised set of themes makes it much easier to see what is available in the codebase, and we can easily specify an alternative theme to use when the context requires without changing the overall styling or structure.
Using Sass Maps
In Sass, things are generally easier to automate if you use one of the built-in data structures like lists or maps. In this case, a map is a better fit as we’re likely to be working with Strings, especially when it comes to creating the various class names a component will require.
Sass features several useful functions for working with maps, most helpfully map-keys
, map-has-key
and map-get
, so we can create a map for a theme and access the properties in a consistent manner:
$ticketArena: (
name: “ticket-arena”,
foreground: #FFFFFF,
background: #20A1B9
…
);
Sass also includes some powerful string interpolation features, allowing us to use variables in selectors and property names:
.btn-t-#{map-get($theme, “name”)} {
// styles
}
It’s not possible to use a string to access a property of a map (so no $themes[$string]
or $$theme-name
style syntax) but maps can contain nested maps, so we can achieve something similar by having a two level data structure. If you’re not sure why this is important, it should all become clear soon…
Example Themes
$themes: (
codepen: (
foreground: #CCCCCC,
background: #1D1F20,
border: #000000,
emphasis: darken(#999999, 10),
subtle: lighten(#999999, 10),
highlight: lighten(#1D1F20, 10),
radius: 3px
),
codepen-light: (
foreground: #1D1F20,
background: #F2F2F2,
border: darken(#F2F2F2, 15),
emphasis: lighten(#1D1F20, 10),
subtle: darken(#1D1F20, 10),
highlight: darken(#F2F2F2, 10),
radius: 3px
)
);
In the example above, you can see how a light variant is provided alongside the standard theme (both created using colours found in the Codepen editor), this allows us to create theme classes to support using light components on dark and vice-versa without needing to provide “inverted” properties in each theme.
Now the theme colours are declared, we can start to use them properly.
A component is responsible for its own theme implementations
With a centralised $theme
map, we can now start the process of automatically generating the classes we need for components. It’s important that the components remain in charge of concrete implementations as there will always be subtleties and nuances that need addressing.
Using our button component as an example, to correctly provide the various theme options we need to use the foreground
, background
, border
and radius
properties.
Here’s where the importance of using nested maps becomes more obvious - first create a mixin called button-theme
which takes one argument, name
. As Sass lacks the ability to use variables to dynamically access variables, without our implementation of $themes
we’d need to manually pass in the various theme maps for our mixin to pull colours from. By using a nested map, we can simply pass the name of the theme as a string and get the relevant map from our $themes
implementation.
@mixin button-theme($name) {
$theme: map-get($themes, $name);
.btn-t-#{$name} {
// properties
}
}
I’ve chosen to use button-theme as the name, so that the actual usage makes sense in plain English:
@include button-theme(‘foo’);
Now we have a mixin, it’s only a small amount of additional code to automatically create all our button themes:
@each $theme in map-keys($themes) {
@include button-theme($theme);
}
This can be done in the same file as the component or you could choose to create a _themes.scss
that creates all the component theme CSS in one place. Personally, I’d keep it all with the component code, but you may feel differently.
Our final code might look something like this:
@mixin button-theme($name) {
$theme: map-get($themes, $name);
.btn-t-#{$name} {
color: map-get($theme, ‘foreground’);
background-color: map-get($theme, ‘background’);
border-radius: map-get($theme, ‘radius’);
border-color: map-get($theme, ‘border’);
// an alternative implementation of border
// where the border property in $theme is
// a list specifying shorthand properties
// border: map-get($theme, ‘border’);
}
}
@each $theme in map-keys($themes) {
@include button-theme($theme);
}
Working with a subset of themes
If a component doesn’t need to implement a version of every theme, create a list of themes to use as a default in the component’s file and iterate over that list instead of the $themes map as a whole. As our mixins can work with any string passed in it doesn’t matter where they come from.
$btn-themes: foo, bar, baz;
@each $name in $btn-themes {
@include button-theme($name);
}
Keep theme properties generic
Reviewing our code above, it’s obvious that we’ve missed the :hover
state. Initial thoughts might be to include a hover
property in our theme, but I’d recommend against using names that relate to a specific usage. Instead favour a more general set of names such as emphasis, subtle and highlight - it’s always possible to expand on these within our mixins by using Sass functions to create slight variations.
Here’s our revised mixin:
@mixin button-theme($name) {
$theme: map-get($themes, $name);
.btn-t-#{$name} {
color: map-get($theme, ‘foreground’);
background-color: map-get($theme, ‘background’);
border-radius: map-get($theme, ‘radius’);
border-color: map-get($theme, ‘border’);
&:hover {
color: map-get($theme, ‘emphasis’),
background-color: map-get($theme, ‘highlight’);
}
}
}
Naming theme properties
For this approach to work, a theme needs to fulfil a contract regarding the colours it will provide and agreeing the naming convention is the difficult part.
It’s best to avoid overly prescriptive names like “heading” or “positive” because they may not be generic enough to make the theme easy to use and understand. When working on the examples for this post I focused on how text editors handle themes to see how they are named and what properties were included to come up with some straightforward suggestions.
I favour keeping the themes small with properties such as foreground, background, border and emphasis. This way we can create properly complementary themes easily and don’t have to worry about providing properties such as foreground—inverted.
If, like in our case, you are building this as part of a UI Library it’s also important to provide helper functions and mixins to ensure that end users can work with themes easily (more on this later).
Working With Defaults
Use Sass' introspection functions
As we are building a library to be used by other projects, we need to consider how these projects can implement their own themes on top of those defined in the library. Sass’ introspection features allow us to check if a variable already exists (the project themes) and alter our definition of $themes appropriately.
You can see how this might work in the example below:
Play with this gist on SassMeister.
Provide functions and mixins to avoid human errors
In addition to defensively defining our library themes, we should also provide a simple interface for working with main $themes data structure.
@function get-theme($name) {
@return map-get($themes, $name);
}
@function add-theme($theme, $name) {
@return map-merge($themes, ( $name : $theme));
}
@function requires($theme, $props) {
@each $key in $props {
@if (not map-has-key($theme, $key)) {
@warn "To use this theme requires #{$key} to be set in the map";
@return false;
}
}
@return true;
}
While get-theme
and add-theme
are fairly obvious, the requires
function may need a bit more explanation.
A component may rely on a set of properties and will produce unexpected output if an expected property is not defined in a theme. Instead of risking unknown results, we can conditionally check if all the requirements are met before calling the mixin for a function:
@if (requires(get-theme(foo), foreground background)) {
/* Include mixin here, we know that all requirements are satisfied */
} @else {
/* Don't include mixin as we know that a required property is missing */
}
To make the development process easier, we’ll also output a notice using @warn so we can see in our console if there are any themes to modify for compatibility.Originally I chose to use @error so everything blew up but there may be cases where we don’t care that a theme will be ignored for a component.
These are themes Jim, but not as we know them
Or something like that. I guess the point I want to address here is how component themes (what this article has been about) fit in with site-wide themes as a whole. I haven’t given as much thought to this mainly because we don’t have any need for global theming just yet. My gut feeling is it wouldn’t be too hard to expand on this solution to easily create specific stylesheets for individual (global) themes.
Live Example
You can see how all the concepts I’ve put forward in this post might be used in the following Pen. I’m sure there are still many rough edges to knock off, and there are bound to be areas that need further thought, but as a method of working we have found it to be very helpful, especially when we can also use React components to specify a theme as a prop.
Any feedback would be appreciated via twitter, I’m @anatomic
See the Pen A Sassy Approach To Theming by Ian Thomas (@anatomic) on CodePen.