Some Extremely Handy `:nth-child` Recipes as Sass Mixins

Avatar of Adam Giese
Adam Giese on (Updated on )

There is no such thing as one-size-fits-all styling. An image gallery with three images might need to be styled differently than an image gallery with twelve. There are some cool tricks that you can use to add some number-based logic to your CSS! Using :nth-child and :nth-last-child, you can get some surprisingly complex information without ever leaving your stylesheet.

This post will assume that you have a basic understanding of how the :nth-child pseudo-selector works. If you need a quick refresher, Chris has a great post covering that topic.

Writing Complex :nth-child() Selectors

You may be aware that along with :nth-child there is the related :nth-last-child. It works exactly the same as :nth-child except that it starts from the end of the parent. For example, you can select the first three children of a parent by using the selector :nth-child(-n + 3). You can use :nth-last-child(-n + 3) to select the last three. Let’s take a look an unordered list for an example.

See the Pen 1. Selecting The Last Three by Adam Giese (@AdamGiese) on CodePen.

Similarly, you can use :nth-last-child(n + 4) to select all children except the last three.

See the Pen 2. Selecting All But The Last Three by Adam Giese (@AdamGiese) on CodePen.

The real magic happens when you start combining :nth-last-child with :first-child. By using both selectors to see if an element is both the first child of the parent and one of the last three elements, you can style the first element of a parent only if there are at most three elements.

li:nth-last-child(-n + 3):first-child {
  /* your styling here */
}

Continuing with our list example, here is a way to visualize how this works:

See the Pen 3. Chaining Together “first-child” and “last three” by Adam Giese (@AdamGiese) on CodePen.

Conversely, you can use li:nth-last-child(n + 4):first-child to select the first item of a list that has four or more items:

See the Pen 4. At Least Three Selector by Adam Giese (@AdamGiese) on CodePen.

Pretty cool, right? But chances are, you’ll want to style more than just the first item in a list. By using the general sibling selector, you can style any list items that follows this first element. Add both selectors separated by a comma, and you can select all elements.

li:nth-last-child(n + 4):first-child,
li:nth-last-child(n + 4):first-child ~ li {
  /* your styling here */
}

See the Pen 5. At Least Four Selector by Adam Giese (@AdamGiese) on CodePen.

This leads to a lot of interesting possibilities! You also don’t need to just limit yourself to counting the total number of elements. By chaining together :first-child and :nth-last child, you can check if any valid :nth-child expression applies to the total number of elements. You can write styling if there are a total number of odd children, if there are exactly seven children, or if there are 3n + 2 children.

One real world use case that this technique can be used for is styling dropdown menus that have been automatically generated by a CMS. For an example, let’s take a look at the code generated by the WordPress wp_nav_menu() function, cleaned up for readability:

See the Pen WordPress Menu nth-child example by Adam Giese (@AdamGiese) on CodePen.

This looks pretty standard. Hover over the Members menu item, and notice the sub-menu items.

Now, let’s look at the same menu with a few more members added:

See the Pen WordPress Menu nth-child example, more children by Adam Giese (@AdamGiese) on CodePen.

Whoa! The styling that worked fine for four items doesn’t scale well to twelve. By applying the previous example, you can style the WordPress default menu purely in the stylesheet: no need to write a custom filter to change the class depending on the number of menu items: you can keep the styling in the stylesheet.

See the Pen WordPress Menu nth-child example, compact by Adam Giese (@AdamGiese) on CodePen.

Here are some other possible use cases.

Limitations

There are a lot of cool things that you can do with :nth-child. However, there are some limitations. For example, you are not able to style the parent, only the children. You can’t add any styling for a ul by the number of li elements it contains.

Another consideration is whether or not to use :nth-child or :nth-of-type. Using :nth-child will select all of the elements, which may or may not be what you want. For example, if you want to add styling to a .post-content based on the number of paragraphs, you would want to use p:nth-of-type. Unfortunately, using this method you would only be able to style children after (and including) the first paragraph tag. If there were an h2 tag at the beginning of the content, you would not be able to style the heading based on the number of p tags. Additionally, nth-of-type only works on elements: you would not be able to check the quantity of a specific class.

I have found that this trick works best when applied to elements with predictable siblings. li is a perfect candidate, since they are the only valid children of ul and ol. Automatically generated content can also work well.

Abstractions

This is a cool trick, but unfortunately the syntax is a little bit hairy. If you are using Sass, however, you can easily add a layer of abstraction to make it much easier to use! This will make the use of these complex selectors much more readable and intuitive.

Since all of these selectors are based on a single pattern, you can start with a general mixin:

@mixin has-nth($expression, $element: '*') {
  &:nth-last-child(#{$expression}):first-child,
  &:nth-last-child(#{$expression}):first-child ~ #{$element} {
    @content;
  }
}

Which would be called in SCSS using something like:

li {
  @include has-nth('n + 4', 'li') { //four or more
    /* your styling here */
  }
}

While this is definitely less cluttered than writing pure CSS, you can add another layer of abstraction by defining a few more mixins. Here is an example of how to write an at-least mixin:

@mixin at-least($quantity, $element: '*') {
  @include has-nth('n + #{$quantity}', $element) {
    @content;
  }
}

These would be called in the code like this:

li {
  @include at-least(4, 'li') { // four or more
    // styling goes here...
  }
}

Definitely an improvement in readability! You can even chain together mixins by nesting them.

Here is a collection of Sass mixins using some complex nth-child logic.

Future Selectors

There are some additional features that are currently in the W3C Editor’s Draft that could greatly increase the flexibility of this technique. One of these is the selector list argument for nth-child. This works by adding an additional optional argument to the nth-child selector. This works similarly to how the nth-of-type works, except that it works for any selector, not just elements. For example, of you could use the selector li:nth-child(odd of .active) to style every other list item with a class of active. Using this feature, we can detect the quantity of a specific class: for example:

li.active:nth-last-child(n + 5 of .active).first-child,
li:nth-last-child(n + 5 of .active).first-child ~ li.active {
  /* styling goes here... */
}

This will style any active list items if there are at least five. You can read more about the selector list here. Browser support is currently very limited, however; it is only available on Safari (both iOS and OSX).

Another feature is the relational pseudo-class selector. This proposed selector accepts a selector list argument and matches if the selector exists relative to the base selector. For example, you can style ul:has(li) to add styling to any lists with at least on li. You can add much more complicated selectors as well: .post-content:has(h1, h2, h3, h4, h5, h6) can style any .post-content with at least one heading element. Using this feature combined with some of the advanced nth-child recipes that we learned about, we can write ul:has(li:nth-last-child(n + 5):first-child) to style any ul with at least five li.

As of now, styling by quantity is mostly useful for styling element with predictable siblings, such as lists or tables. If these two features become supported, then this technique will be much more flexible.