Looking to learn some advanced Sass functionalities, such as lists, nested lists, maps, and Sass methods? Jump in, you'll learn all of the above and then we'll have fun drawing CSS Pokemon in part two.
Who would've thought, just a few years back, that "front-end" developers would soon get an "engineer" right beside it? And engineers they are becoming. In a sea of trivial and complex javascript frameworks, task runners, package managers, pre and post processors, along with increased need for abstraction, optimization, efficiency, and modularity - an average front-end developer needs to know a lot to stay afloat.
In this blog post, we'll focus on our favorite CSS preprocessor - Sass, and more specifically - its list and map functionalities. Because - engineering.
Sass Lists
Alas, "arrays" in CSS! One of the first things to-be-programmers learn is data structures. The principle is always the same - arrays are a kind of data structure that can store a fixed-size sequential collection of elements of the same type. Sass being a modern "language" does not require you to code things strictly, so we can kick out the "fixed-size" and "same type" out of the definition.
How Do I Define a List?
Easy peasy.
$pokemons: jigglypuff slowpoke gengar;
or
$pokemons: jigglypuff, slowpoke, gengar;
or
$pokemons: "jigglypuff", "slowpoke", "gengar";
Reasonable for non-developers, batshit crazy for developers - indexing starts at 1 instead of 0 like in all other languages. The important thing to remember is that everything in Sass is, in fact, a list. This means we can use list functions on strings, for example.
$ash: "Gottha catch them all!"
length($ash) # Length is 1
What happens if we drop the quotes?
$ash: Gottha catch them all!
length($ash) # Length is 4
Sass List Functions
Sass is inherently providing us with a nice set of functions to access and manipulate items from data type structures. These are the list functions, as sourced from the SassScript documentation:
length($list) : Returns the length of a list.
nth($list, $n) : Returns a specific item in a list at the nth value.
set-nth($list, $n, $value) : Replaces the nth item in a list.
join($list1, $list2) : Joins together two lists into one.
append($list1, $val) : Appends a value onto the end of a list.
zip($lists…) : Combines lists into a single multidimensional list.
index($list, $value) : Returns the position of a value within a list. (You can also use this to determine if a value exists within a list)
list-separator(#list) : Returns the separator of a list.
Let's try out some of them on our Pokemon list.
nth($pokemon-types, 1)
// Results in jigglypuff
nth($pokemon-types, -1)
// Results in gengar
Let's do a for loop using length method.
@for $i from 1 through length($pokemons) {
.pokemon-#{$i} {
&:after {
content: "#{$i}. #{nth($pokemons, $i)}";
}
}
}
css output.pokemon-1:after {
content: "1. jigglypuff";
}
.pokemon-2:after {
content: "2. slowpoke";
}
.pokemon-3:after {
content: "3. gengar";
}
Each loop is much better for sass lists since they don't require the length information.
@each $monster in $pokemons {
.pokemon-#{$monster} {
background: image-url("img/pokemons/#{$monster}.png") no-repeat;
}
}
css output.pokemon-jigglypuff {
background: url('/images/img/pokemons/jigglypuff.png') no-repeat;
}
.pokemon-slowpoke {
background: url('/images/img/pokemons/slowpoke.png') no-repeat;
}
.pokemon-gengar {
background: url('/images/img/pokemons/gengar.png') no-repeat;
}
You can also append other Pokemon to the Pokemon list.
// Pokemon to append
$one-more-pokemon: onyx;
// Appending $pokemons:
append($pokemons, one-more-pokemon, space);
// Outputs: jigglypuff slowpoke gengar onyx
Enter the Matrix
Nesting, baby.
Just as arrays can contain other arrays, lists can contain other lists. Every time you separate items with a comma (in a list), you're actually building a nested list. Let's take a look at a list of two lists.
$team-rocket: jessie ekans, james koffin;
An easier way to write and read a list is to use multiple lines.
$list: item-1 item-2 item-3,
item-4 item-5 item-6,
item-7 item-8 item-9;
or using parenthesis:
$list: ((item-1 item-2 item-3),
(item-4 item-5 item-6),
(item-7 item-8 item-9));
This is also called a 3 by 3 matrix.
Accessing Nested Lists
Our $team-rocket list has a person - Pokemon pairs, in a way it looks a bit like an associative array, but it’s not, it’s a nested list. The best way for accessing such a list is an each loop where we iterate through the list, and interpolate the variable using nth() list function. Here’s an example.
@each $pair in $team-rocket {
.team-rocket--#{nth($pair, 1)} {
background-image: url('img/#{nth($pair, 2)}.png') no-repeat;
}
}
css output.team-rocket--jessie {
background-image: url("img/ekans.png") no-repeat;
}
.team-rocket--james {
background-image: url("img/koffin.png") no-repeat;
}
List Use Cases
Rounded Corners
We can easily pass a list to a mixing to create a more flexible one.
@mixin round-corner ($corners: 0 0 0 0) {
$top-left: nth($corners, 1);
$top-right: nth($corners, 2);
$bottom-right: nth($corners, 3);
$bottom-left: nth($corners, 4);
border-top-left-radius: $top-left;
border-top-right-radius: $top-right;
border-bottom-right-radius: $bottom-right;
border-bottom-left-radius: $bottom-left;
}
.rounded {
@include round-corner(1rem 2rem 3rem 4rem);
}
css output.rounded {
border-top-left-radius: 1rem;
border-top-right-radius: 2rem;
border-bottom-right-radius: 3rem;
border-bottom-left-radius: 4rem;
}
Background Photos
If you would like to have a special class that links to the appropriate background photo in your project directory, you can just add the filename to the $photos list, and create an each loop that outputs classes.
$photos: jigglypuff slowpoke gengar;
@each $photo in $photos {
.background--#{$photo} {
background-image: url('photos/#{$photo}.jpg');
}
}
css output.background--jigglypuff {
background-image: url("photos/jigglypuff.jpg");
}
.background--slowpoke {
background-image: url("photos/slowpoke.jpg");
}
.background--gengar {
background-image: url("photos/gengar.jpg");
}
Generating Classes
If, for whatever reason, you would like to generate classes like so:
css output.home .home--navigation,
.about .about--navigation,
.contact .contact--navigation {
display: flex;
}
You could achieve it with a class list and an empty list where you would append the generated classes:
$pages: home about contact;
$selector: ();
@each $item in $pages {
$selector: append($selector, unquote('.#{$item} .#{$item}--navigation'), 'comma');
}
#{$selector} {
display: flex;
}
Sass Maps
Dictionary, hash, associative arrays, it is known by many names. We can look at a Sass map as a list of key/value pairs.
How Do I Define a Map?
Sass map uses parentheses as external delimiters, colons to map keys and values, and commas to separate key/value pairs. Here's an example of a valid map:
$team-rocket-map: (
jessie: ekans,
james: koffin
);
Having stored multiple key/value pairs at some point you’ll need to retrieve that information. If you need to find the value of a key use the function map-get(). We’re going to pass two parameters to it: the name of the map and then the key.
.rocket:after {
content: map-get($team-rocket-map, james);
}
css output.rocket:after {
content: koffin;
}
Sass also provides the function map-has-key(). This helper method checks whether a given key exists, and it outputs a warning to the developer if not. Let's add a warning to the Sass code above:
.rocket:after {
@if map-has-key($team-rocket-map, brock) {
content: map-get($team-rocket-map, brock);
}
@else {
content: 'Sorry, but team-rocket-map doesn\'t have this key!';
}
}
css output.rocket:after {
content: 'Sorry, but team-rocket-map doesn't have the required key!';
}
It might seem like an overkill, and it probably is. Use this only in complex mixins. Check out the @error, @warn and @debug directives too. And for those who are starstruck with if-else conditional in Sass... Oh yes, you can use it all you want now :)
Sass Map Functions
These are the map functions, as sourced from the SassScript documentation:
map-get($map, $key) : Returns the value in a map associated with a given key.
map-merge($map1, $map2) : Merges two maps together into a new map.
map-remove($map, $keys…) : Returns a new map with keys removed.
map-keys($map) : Returns a list of all keys in a map.
map-values($map) : Returns a list of all values in a map.
map-has-key($map, $key) : Returns whether a map has a value associated with a given key.
keywords($args) : Returns the keywords passed to a function that takes variable arguments.
Map Use Cases
How to Loop Through a Map and Generate Classes
We'll create a map plotting Pokemon trainers to their favorite Pokemon.
$trainers: (
ash: pikachu,
misty: magikarp,
brock: onyx
);
Next, we'll make an each loop that will go through every key and value in a map, and output the keys in a class name, and values into its content!
@each $name, $pokemon in $trainers {
.trainer--#{$name} {
content: $pokemon;
}
}
css output.trainer--ash {
content: "pikachu";
}
.trainer--misty {
content: "magikarp";
}
.trainer--brock {
content: "onyx";
}
Multiple Values for Amazeball Buttons Module
Moving on. It’s possible to give a key more than one value. This is done by using clippings and entering multiple values with a comma separator. This is a great way to transfer styles for variants of a module, in this example - button.
In our little snippet, the first value will be the background color of the button, the second will be the text color, and finally padding. We will then loop through the keys, and we'll reference the values as a $styles object. We can easily fetch values using nth() function. Let's do this.
$styles: (
error: (red, white, 3rem),
success: (green, white, 2rem),
warning: (yellow, black, 1rem)
);
.button {
display: inline-block;
background-color: lightgray;
color: darkgray;
padding: 0.5rem;
@each $type, $style in $styles {
$background-color: nth($style, 1);
$font-color: nth($style, 2);
$padding: nth($style, 3);
&--#{$type} {
background-color: $background-color;
color: $font-color;
padding: $padding;
}
}
}
css output.button {
display: inline-block;
background-color: lightgray;
color: darkgray;
padding: 0.5rem;
}
.button--error {
background-color: red;
color: white;
padding: 3rem;
}
.button--success {
background-color: green;
color: white;
}
.button--warning {
background-color: yellow;
color: black;
padding: 1rem'
}
Handling Layers (z-index)
The infamous z-index. Lots of frontend developers are struggling with it in a sense that they don't have any particular design pattern around it. Usually, z-index is set to 1 if we want a particular layer to be "on top of" something else, 2 if we want another layer to really be "on top of" something else, then 9, then 99, then 9999, then 99999999... and so on. It's easy to get lost. This is where Sass maps can help us.
Let’s start with a $layer map. We can name the keys with something meaningful, such as "modal", "tooltip", "offcanvas", and so on.
$layer: (
default: 1,
offcanvas: 100,
dropdown: 150,
modal: 200,
tooltip: 500
);
Next, we can create a layer function that returns the appropriate z-index, let's say for a dropdown.
@function layer($level) {
@return map-get($layer, $level);
};
.dropdown {
z-index: layer(dropdown);
}
Surely, you can skip the function and directly use the map-get() function inside the class, but this way you can use the error handling which we omitted for this example. This is the output:
css output.dropbox {
z-index: 500;
}
Font Base Styles
A good project has it's own configuration file. One example would be base typography, where you would define base font color, family, size and line height as variables. Perhaps a better way would be to use a map for the said attributes.
$font: (
color: $base-color,
family: (Alegreya, Roboto),
size: 18px,
line-height: 1.6
);
body {
color: map-get($font, color);
font-family: map-get($font, family);
font-size: map-get($font, size);
line-height: map-get($font, line-height);
}
Ta daah!
Breakpoints <3
Personal favorite. It’s superb to have a place for the breakpoints that are used throughout the project. In this way, similar to the z-index hack, you can have an overview of the breakpoints. Also, you can change the behavior throughout your project by changing only the breakpoints map. You can also easily add new breakpoints, or remove extra ones.
So let’s start with a map called $breakpoints.
Our goal is to use breakpoints with implicit names instead of hard pixel values in an element. Because of this, we need a mixin that will output the value of the stated name. I called the mixin "breakpoint" and passed $breakpoints as a parameter. With $type I get the pixel value of the expected breakpoint. First things first, let's create the map.
$breakpoints: (
small: 0,
medium: 640px,
large: 1024px,
xlarge: 1200px,
xxlarge: 1440px
);
Next, we'll do a mixin called breakpoint, and include the mixin in a .navigation--mobile class like so:
@mixin breakpoint($breakpoint) {
$breakpoint: map-get($breakpoints, $breakpoint);
@media screen and (min-width: $breakpoint) {
@content;
}
}
.navigation--mobile {
display: block;
@include breakpoint(medium) {
display: none;
}
}
css output.navigation--mobile {
display: block;
}
@media screen and (min-width: 640px) {
.navigation--mobile {
display: none;
}
}
Now I Know Everything, Why Would I Need "Part Two"?
Now that we've covered the basics, there's much more to explore using Sass, but most importantly - you always need to practice. In part two we will use this knowledge, and expand it to a mini project where we'll create simple, colorful, pixelized CSS pocket monsters. Here's a small teaser - all of the drawings will be done with box shadows. Stick around!
Part two is online, you can read it up here!