Recently, I’ve seen a recurring error with how
:nth-of-type() selectors are being used, so I wanted to host an anonymous algebra moment on these, my favorite CSS Level 3 selectors.
Frequently, I see
:nth-child() selectors used for making columns:
From here, I see a lot of selectors like this:
Here’s why that is incorrect: the browser starts counting at 1 (the first item) and checks item each to see if that element matches the pattern (an+b) where “a” is the number you gave it, “n” is a set of all integers, and “b” is the offset that I’ve seen omitted (which defaults to zero). This is what happens:
As you can see:
:nth-child(1n)matches every single column. And why wouldn’t it? The set described as (1n+0) where n is a set of all integers results in […0, 1, 2, 3, 4, 5, 6, 7, …]. In other words, this selector doesn’t actually do anything, you could stick with the element or class selector.
:nth-child(2n)matches every other column because the set described as (2n+0) is […0, 2, 4, 6, 8, …]
- 4n and 5n look safe, but what if there’s a second row?
TL;DR: The important take-away here is that the repetition is the a in
an+b. So a is the total number of columns in your layout, not the “column number” you’re trying to address. The column number is the “+b”, The Offset!
What you want is this:
:nth-child(6n+1)matches the first column (the first, seventh, thirteenth, nineteenth, … elements)
:nth-child(6n+6), which can be written simply as
:nth-child(6n), matches the last column (the sixth, twelfth, eighteenth, … elements)
- Why can the “+6” column number (the offset) be omitted? The set of (6n+6) is […-6, 0 6, 12, 18, 24, 30…] and the set of (6n) is […-12, -6, -, 6, 12, 18, 24…]; they are the same.
- If dropping the offset might be confusing when re-reading code, then leave it! It does no harm, and might even be a good idea to retain.
And this stacks vertically as expected:
But the way I had it (usually) worked! What’s wrong with that? Two reasons: it doesn’t always work, and pride. 😉
Look at it this way:
:nth-child(1n), the middle column is selected, too!
- The reason this may go unnoticed is that the
:nth-child(2n)is likely written after the 1n rule, with the same selector specificity, so it overrides the first.
Two problems arise when selecting
- The first column of the second row is also selected. Why? It’s the fourth element, which is in the set defined as (2n).
- That second column on the fourth row is not selected. Why? It’s the eleventh element, which is not in the set defined as (2n).
- Worse, with an odd number of columns, even/odd flips on each row! Count it out:
- You’d run into the same changing “rhythm” with other numbers of columns or a different incorrect interval. (This might be your intention, if you were making a pattern, and it’s cool that CSS lets us do that so easily, but ya gotta know the rule before you break it.)
Why am I pestering you with this?
- Even if the site renders correctly now, imprecise code is difficult to maintain.
- If someone is inspecting your code, I want them to see excellent examples of your work.
:nth-child()is one of my favorite selectors because of its power, which I encourage to be used for carefully constructed good.
- Because if you don’t, I’ll make you use dastardly class names like
.three-column-layout--column-firstwhich seem to be all the rage right now.
- Some folks out there think that this is the right way to go. The only convincing argument I see in favor of this approach is that it allows developers to avoid learning.
- Using intelligent selectors is far more graceful.
Does it matter if only one row is used in the layout?
:nth-child(1n) selects every column and the
:nth-child(2n) selects every even column, overriding the first rule. That’s inefficient and makes debugging and careful overrides that much harder.
And what about
element:nth-of-type()uses the same math as
element:nth-child(), but anything that isn’t
elementwill be skipped in the counting in addition to not being selected. In other words, when counting the addresses of elements to select
spanwould not be counted as an element in the set.
- I frequently see
nth-child()would have been sufficient. As such,
nth-child()is probably the safer option (unless you need type specificity in counting) because it will make erroneous or disorganized HTML output more obvious.
- Bottom line: specificity and consistency. Using the simplest selector possible for the task at hand is a CSS best practice. Straying from consistency is my biggest pet peeve.