Working with lists and @each loops in Sass with the index and nth function
In Chapter 8 of Sass and Compass for Designers the process of creating loops with an @each
loop is covered.
Part of the chapter deals with how to index a list and iterate over values of that list while also making use of the current position that a value exists in within a loop. So, for example:
$colors-list: #111 #222 #333 #444 #555;
@each $current-color in $colors-list {
$i: index($colors-list, $current-color);
.stuff-#{$i} {
color: $current-color;
}
}
Here we have a list of colours set as a variable and then we want to create a new class based on the current loop count and use each colour within the list of colours within each iteration of the loop. That sounds more complicated than it is. Here’s how that prior code actually compiles into CSS:
.stuff-1 {
color: #111111;
}
.stuff-2 {
color: #222222;
}
.stuff-3 {
color: #333333;
}
.stuff-4 {
color: #444444;
}
.stuff-5 {
color: #555555;
}
Make more sense now? We’re incrementing the selector and amending the colour within. That’s fine while we only have one set of colours to use or base manipulations upon but that might not always be the case.
Let’s suppose we have a situation where we have more than one list of colours we need to make use of and there is no obvious correlation between them (in that we can’t just manipulate the starting colour with a function such as lighten($color1, 10%)
to get there).
How about a bunch of tiled panels. Each has a color and corresponding background-color and border (or maybe a box-shadow color or text shadow – whatever needs a colour). Here’s how Sass can solve this and keep our style sheets DRY:
$background-colors-list: #4BC2A5 #606F40 #6648CA #6B6022 #2F54E9;
$border-colors-list: #ff9900 #77ff66 #73C227 #C23E50 #7BAFC2;
$colors-list: #111 #222 #333 #444 #555;
@each $current-color in $colors-list {
$i: index($colors-list, $current-color);
.stuff-#{$i} {
color: $current-color;
background-color: nth($background-colors-list, $i);
border: 1px solid nth($border-colors-list, $i);
}
}
Here’s what’s happening. We’ve created three lists as variables, each containing five unrelated colours as hex values.
Then, as in the first example we are using the index
function to count the number of items (actually not strictly true but more of that in a moment) in the list and set the variable $i
as the current index number – the position the value is within the list (so each time the loop runs we know the count number). Because we have this we can then use that same incrementing value to pluck a different colour from the other lists. We do this using the nth
function.
What the nth?
The nth
function allows a value to be plucked from a certain position in a list – like choosing the position from an array in JavaScript or programming languages. In this instance, rather than passing the nth position we want as an integer, we are using our counter variable as a kind of ‘dynamic’ grabber to pluck the corresponding colour value from the different lists.
In this example we are using the technique twice, once on the $background-colors-list
and again on the $border-colors-list
and then using the value on the associated CSS property.
And with that, we get the following compiled CSS:
.stuff-1 {
color: #111111;
background-color: #4bc2a5;
border: 1px solid #ff9900;
}
.stuff-2 {
color: #222222;
background-color: #606f40;
border: 1px solid #77ff66;
}
.stuff-3 {
color: #333333;
background-color: #6648ca;
border: 1px solid #73c227;
}
.stuff-4 {
color: #444444;
background-color: #6b6022;
border: 1px solid #c23e50;
}
.stuff-5 {
color: #555555;
background-color: #2f54e9;
border: 1px solid #7bafc2;
}
For creating 5 styles, I’d concede that aside from bragging rights, doing this probably doesn’t necessarily seem worthwhile. However, with a situation of 10 or more to produce, it starts to make a lot more sense.
Gotcha
One thing that caught me out using the index
function is that it searches the list for a value – as soon as it finds the value, it returns it as the index number. Ordinarily, that’s fine. However, if you ever want to iterate over a few lists (as above) and there are duplicate values in the list, things might not end so well. Let me show you how things can go wrong. Here’s our prior code block but I’ve duplicated a colour in the $colors-list
(position 1 and 3 are the same):
$background-colors-list: #4BC2A5 #606F40 #6648CA #6B6022 #2F54E9;
$border-colors-list: #ff9900 #77ff66 #73C227 #C23E50 #7BAFC2;
$colors-list: #111 #222 #111 #444 #555;
@each $current-color in $colors-list {
$i: index($colors-list, $current-color);
.stuff-#{$i} {
color: $current-color;
background-color: nth($background-colors-list, $i);
border: 1px solid nth($border-colors-list, $i);
}
}
This is how that compiles:
.stuff-1 {
color: #111111;
background-color: #4bc2a5;
border: 1px solid #ff9900;
}
.stuff-2 {
color: #222222;
background-color: #606f40;
border: 1px solid #77ff66;
}
.stuff-1 {
color: #111111;
background-color: #4bc2a5;
border: 1px solid #ff9900;
}
.stuff-4 {
color: #444444;
background-color: #6b6022;
border: 1px solid #c23e50;
}
.stuff-5 {
color: #555555;
background-color: #2f54e9;
border: 1px solid #7bafc2;
}
See what happened there? We got a replication of the selector and the colour within. The reason being that when the colour is indexed, the list is searched from the beginning and the index value returned is the first place that value is found. In our example, the #111 value was indexed and found in position 1 (at which point Sass finished its search and used that value).
When I encountered this problem I came unstuck. Thankfully a cry for help on the Sass GitHub pages bore fruit. Mr Eppstein (Compass creator and Sass maintainer) suggested this solution, using a @for
loop instead:
$background-colors-list: #4BC2A5 #606F40 #6648CA #6B6022 #2F54E9;
$border-colors-list: #ff9900 #77ff66 #73C227 #C23E50 #7BAFC2;
$colors-list: #111 #222 #111 #444 #555;
@for $i from 1 through length($colors-list) {
.stuff-#{$i} {
color: nth($colors-list, $i);
background-color: nth($background-colors-list, $i);
border: 1px solid nth($border-colors-list, $i);
}
}
Here we are using a @for
loop instead. The real magic part here is the use of the length
function to set the amount of loops to the length of the items in the list. With that we can use the nth
function again to pick the colour now too.
As you would hope, here is the output:
.stuff-1 {
color: #111111;
background-color: #4bc2a5;
border: 1px solid #ff9900;
}
.stuff-2 {
color: #222222;
background-color: #606f40;
border: 1px solid #77ff66;
}
.stuff-3 {
color: #111111;
background-color: #6648ca;
border: 1px solid #73c227;
}
.stuff-4 {
color: #444444;
background-color: #6b6022;
border: 1px solid #c23e50;
}
.stuff-5 {
color: #555555;
background-color: #2f54e9;
border: 1px solid #7bafc2;
}
Very interesting article !
I am trying to do it with another more semantic way :
$theme_green: (success successGreen) (error errorGreen) (warn warnGreen);
$theme_red: (success succesRed) (error errorRed);
$themes_to_compile : ($theme_green $theme_red);
@each $_theme in $themes_to_compile{
@each $_colorDef in #{$_theme}{
// there I can’t access to :
// $_colour: nth($_colorDef, 2);
}
}
Do you see why ? Maybe I am missing something ?