Earlier this week, I posted this pen on Codepen as a means of documenting a few interesting things I had learnt whilst building a multi-theme application. The pen got a respectable number of views and hearts, so I thought I would follow it up with a detailed blog posts explaining what I'm doing, and why I'm doing it. I've also made some optimisations to the original code at the end.
In this post I'm going to show you how to:
- Create a reusable tab component with several states.
- Style a navigation bar with a gradient which changes on a theme by theme basis.
- Avoid writing the same code multiple times with minor differences.
Sass Maps
Sass Maps are one of my favourite features in Sass. A map is essentially a Hash (sometimes referred to as an associative array). A Hash is a key value pair where the value could be a string (message: "Hello"
), a number (count: 2
), or another Hash (people: { name: "Rob" }
).
Maps are a fantastic way to define multiple properties on a single variable ideal for managing colour palettes or themes.
Here's are two examples:
// Sass Map containing a number of different values.
$font-weight: (
light: 300,
book: 400,
semibold: 600,
bold: 700
);
// A Sass Map with nested Sass Maps.
$themes: (
default: (
base: $midnight,
dark: darken($midnight, 10%)
),
books: (
base: $pomegranate,
dark: darken($pomegranate, 10%)
)
);
You can loop through a Sass Map using @each
, and access the values inside a Sass map using the map-get
function.
// Looping through a one-dimensional map
@each $weight, $value in $font-weights {
.u-text-#{$weight} {
font-weight: $value;
}
}
// Looping through a nested map
@each $name, $palette in $themes {
.theme--#{$name} {
color: map-get(map-get($themes, $palette), base);
&:hover {
color: map-get(map-get($themes, $palette), dark);
}
}
}
As you can see looping through a nested map is not very readable. This little function I found on this post by Tom Davies, makes accessing nested attributes a much cleaner experience:
@function palette($palette, $tone: "base") {
@return map-get(map-get($themes, $palette), $tone);
}
@each $name, $color in $themes {
.theme--#{$name} {
color: palette($color);
&:hover {
color: palette($color, dark);
}
}
}
Much better!
Using rgba() for stateful styling
You may be used to seeing and doing this:
.tabs--default .tab {
background: $midnight;
&:hover { background: lighten($midnight, 10%); }
&:active { background: darken($midnight, 10%); }
}
.tabs--books .tab {
background: $midnight;
&:hover { background: lighten($pomegranate, 10%); }
&:active { background: darken($pomegranate, 10%); }
}
// Repeat for remaining themes
Rather than defining unique colour values for each of the themes' various states, we can use rgba
to apply a transparent black or white tint when required.
// Set the background on the container instead
.tabs--default { background: $midnight; }
.tabs--books { background: $pomegranate; }
.tab {
&:hover { background: rgba(255, 255, 255, .1); } // Lighten by 10%
&:active { background: rgba(0, 0, 0, .1); } // Darken by 10%
}
Taking things one step further
After publishing the pen, I spent time breaking things down further. Here's a number of improvements that could be made.
Separate Stateful Styling
First thing we're going to do is break the stateful styling (:hover/:active) away from the .tab
component. Doing this allows you to easily re-use this pattern anywhere it's needed.
// ../scss/component/_tab.scss
.tab {
display: block;
padding: .75em 1.5em;
}
// ../scss/utility/_state-shade.scss
.u-state-shade {
transition: background-color .125s;
&:hover {
@include shade-light;
}
&:active {
@include shade-dark;
text-decoration: underline;
}
}
You could of course break the .u-state-shade
and the stateful styling into separate classes if you're going to have numerous variations of this pattern. You may also choose to create a utility class called .u-transform-background
, or something similar which may result in numerous utility classes for each duration, property, or duration/property combination. Easily doable with Sass maps, but I'd recommend shying away from doing that kind of thing.
Create Theme Partials
Many prefer to include these kind of things on the individual element itself. However, I think specifying theme related style in a separate partial makes more sense. It's cleaner, more readable, and you know if there's something I need to adjust on a theme by theme basis it's going to need to be done inside the theme file.
// ../scss/partial/_theme.scss
.theme__gradient {
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, .1) 100%;
}
@each $name, $palette in $themes {
.theme--#{$name} {
.theme__background {
background-color: palette($name);
}
.theme__link.is-active {
color: palette($name);
}
}
}
- Now we can apply a darkened gradient to anything and it'll just work as long as you apply backgrounds to elements using
background-color
rather thanbackground
. - We can add
.theme__link
to any element we want to inherit the theme colour when the.is-active
class is applied to it. - The theme's base
background-colour
can be applied anywhere it needs to be.
The theme partial can be extended to include things like theme specific headings or buttons.
The theme names don't have to be named after each section, you could use generic colour names. Infact, if you've got lots of sections, and by that I mean more than 15, and some of them use the same palette you'd probably be better off with using .theme--green
, .theme--red
, etc. to reduce duplication.
Closing Thoughts
View the finished version with all the optimisations discussed.
There is a fine line between optimisation and creating an overly complex, utterly unreadable codebase. I try to keep my Sass as close to vanilla CSS as possible, using Sass's features when it makes something more readable, or reduces repetition.
Hopefully you've got something out of this post, please let us know if you'd like to see more of this thing from me in the future.