Roma’s Unpolished Posts

A Christmas Tree Selector: Prototyping the :nth-sibling() with CSS Nesting

Published on:
Categories:
CSS Nesting 4, CSS Selectors, CSS 40
Current drink:
Yunnan tea

While working on another article, I stumbled upon a case that I never could achieve in any good way: an ability to select repeating groups of elements.

We have :nth-child(), but it only allows us to select one specific element, and while we can do things like Quantity Queries (article by Heydon Pickering), :nth-child() really allows us to only select a finite group of elements, or repeating sequences of a single selected element.

What I want is: to be able to select groups like A-A-A-B-B-B-A-A-A-B-B-B and so on. I’m not alone in wanting something like that: there is a CSSWG issue with comments from Amelia Bellamy-Royds and Johannes Odland thinking about a hypothetical :nth-sibling() pseudo-class that could allow us to achieve it.

But right now — we don’t have a good way to do this. However, I did spend some time trying to come up with something that could be used for prototyping it instead.

A List of :nth-child() Pseudo-Classes

If we were to express the above A-A-A-B-B-B-… grouping with the :nth-child(), we will have to repeat it for every element of the group:

.example1 {
	li:nth-child(6n+1),
	li:nth-child(6n+2),
	li:nth-child(6n+3) {
		background: var(--GREEN);
	}
}
  1. A
  2. A
  3. A
  4. B
  5. B
  6. B
  7. A
  8. A
  9. A
  10. B
  11. B
  12. B
A live example showing an ordered list of twelve items, where the first three items, as well as those from seventh to ninth, have a green background.

It is a bit cumbersome, but it works:

  1. First, we need to determine the periodicity — at which point does our pattern repeat? In the above case, every sixth element is the same, so we’re using the 6n inside the :nth-child().
  2. Then, as we want to select the first three elements of every group, we have to repeat the selector for every +1, +2, and +3. And if we wish to invert the logic and select the elements from the fourth to the sixth, we could just use +4, +5 and +6.

As you can guess, if we would like to have a group of ten selected elements, we would have to repeat ourselves ten times! Which is far from ideal. This is where we can introduce native CSS Nesting.

A Christmas Tree Selector

We can omit a bit of repetition if instead of using a separate :nth-child() we will use only one of them, but then use a next-sibling combinator:

.example2 {
	li:nth-child(6n+1) {
		&,
		& + *,
		& + * + * {
			background: var(--GREEN);
		}
	}
}
  1. A
  2. A
  3. A
  4. B
  5. B
  6. B
  7. A
  8. A
  9. A
  10. B
  11. B
  12. B
A live example showing an ordered list of twelve items, where the first three items, as well as those from seventh to ninth, have a green background.

Two important notes:

  1. This is likely to be more performance-expensive than a regular :nth-child(). While performance of selectors is usually very fast these days, :nth-child() and sibling selectors can be potentially harmful for large DOM trees, especially when the nodes are added and removed dynamically.

  2. This makes sense only with native CSS nesting: when using preprocessors, just using the repeated :nth-child() should be more performant (this is only speculation; actual testing should be done).

Why did I call this a “Christmas Tree” selector? Let’s say we will want to select the abovementioned group of 10 elements and format our code a bit differently:

.example3 {
	li:nth-child(20n+1) {
		         &,
		        &+*,
		       &+*+*,
		      &+*+*+*,
		     &+*+*+*+*,
		    &+*+*+*+*+*,
		   &+*+*+*+*+*+*,
		  &+*+*+*+*+*+*+*,
		 &+*+*+*+*+*+*+*+*,
		&+*+*+*+*+*+*+*+*+*
		         {
			background: var(--GREEN);
		}
	}
}
  1. A
  2. A
  3. A
  4. A
  5. A
  6. A
  7. A
  8. A
  9. A
  10. A
  11. B
  12. B
  13. B
  14. B
  15. B
  16. B
  17. B
  18. B
  19. B
  20. B
  21. A
  22. A
  23. A
  24. A
  25. A
  26. A
  27. A
  28. A
  29. A
  30. A
  31. B
  32. B
  33. B
  34. B
  35. B
  36. B
  37. B
  38. B
  39. B
  40. B
A live example showing an ordered list of fourty items, where the first ten items, as well as those from 21st to 30th, have a green background.

Yeah. Not super practical, but, in my opinion, better than a list of repeated :nth-child() where we have to adjust every number if we want to adjust how it is applied!

And it looks neat.

Maybe you could even spot an owl hidden somewhere between the branches of this tree!

Further Optimizations

When thinking about this, I thought of one optimization that we could do to reduce the number of lines of code (but not the complexity, really): combine both methods:

.example4 {
	li:nth-child(20n+1),
	li:nth-child(20n+6) {
		    &,
		   &+*,
		  &+*+*,
		 &+*+*+*,
		&+*+*+*+*
		    {
			background: var(--GREEN);
		}
	}
}
  1. A
  2. A
  3. A
  4. A
  5. A
  6. A
  7. A
  8. A
  9. A
  10. A
  11. B
  12. B
  13. B
  14. B
  15. B
  16. B
  17. B
  18. B
  19. B
  20. B
  21. A
  22. A
  23. A
  24. A
  25. A
  26. A
  27. A
  28. A
  29. A
  30. A
  31. B
  32. B
  33. B
  34. B
  35. B
  36. B
  37. B
  38. B
  39. B
  40. B
A live example showing an ordered list of fourty items, where the first ten items, as well as those from 21st to 30th, have a green background.

If the count of elements in our group is not a prime number, we can express our desired “group” as a product of two or more nested selectors! In the above case, we can use just two :nth-child() and a smaller Christmas tree to achieve our group of ten, as 2×5=10.

And if we can have our group as a product of more than two numbers, things can be simplified even more, like for selecting twelve elements it can be expressed as 2×3×2:

.example5 {
	li:nth-child(24n+1),
	li:nth-child(24n+7) {
		    &,
		  &+*+*,
		&+*+*+*+* {
			 &,
			&+*
			 {
				background: var(--GREEN);
			}
		}
	}
}
  1. A
  2. A
  3. A
  4. A
  5. A
  6. A
  7. A
  8. A
  9. A
  10. A
  11. A
  12. A
  13. B
  14. B
  15. B
  16. B
  17. B
  18. B
  19. B
  20. B
  21. B
  22. B
  23. B
  24. B
  25. A
  26. A
  27. A
  28. A
  29. A
  30. A
  31. A
  32. A
  33. A
  34. A
  35. A
  36. A
  37. B
  38. B
  39. B
  40. B
  41. B
  42. B
  43. B
  44. B
  45. B
  46. B
  47. B
  48. B
A live example showing an ordered list of fourty eight items, where the first twelve items, as well as those from 25th to 36th, have a green background.

Now we have just two happy little trees.

The Need for :nth-sibling()

As demonstrated by the above cases, even though we can try to optimize the selectors, they’re still a bit too complicated. I won’t use them in production, and won’t recommend doing so as well.

With all the goodness that we got recently in CSS, including things like CSS Scopes, CSS Nesting, :nth-child(… of …), and so on, an ability to easily select these groups of selectors seems like a big missing piece.

Over the years, I did come upon many cases when I wanted to have something like that. If you did as well — I urge you to post them to the abovementioned CSSWG issue and share your thoughts about the existing proposals and their syntax — will it apply to your cases? Is there still something missing?

Please share your thoughts about this on Mastodon!