Reducing style repetition with Sass Maps and RGBA

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 refered 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 single-dimentional 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 color 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.

Seperate 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 seperate 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 ralated style in a seperate partial makes more sense. It’s cleaner, more readble, 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);
        }
    }
}
  1. 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 than background.
  2. We can add .theme__link to any element we want to inherit the theme colour when the .is-active class is applied to it.
  3. 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 readble, 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.