Using CSS Mod Queries with Range Selectors

Recently, I was asked to build a simple list that would display in a grid—one that could start with a single element and grow throughout the day, yet alway be tidy regardless of the length. So, as you do sometimes when you’re busy with one thing and asked if you can do something completely different, I tried to think of any reason why it couldn’t be done, came up blank, and distractedly said, “Yes.”

At the time, I was working on a London-based news organization’s website. We’d spent the previous year migrating their CMS to the Adobe AEM platform while simultaneously implementing a responsive UI—both big improvements. Since that phase was complete, we were starting to focus on finessing the UI and building new features. The development project was divided into a number of small semiautonomous teams. My team was focusing on hub pages, and I was leading the UI effort.

Each hub page is essentially a list of lists, simply there to help readers find content that interests them. As you can imagine, a news website is almost exclusively made of content lists! A page full of generic vertical lists would be unhelpful and unappealing; we wanted readers to enjoy browsing the content related to their sphere of interest. Sections needed to be distinct and the lists had to be both individually distinguishable and sit harmoniously together. In short, the visual display was critical to the usability and effectiveness of the entire page.

That “simple list” I said I’d build would be high profile, sitting in its own panel near the top of a hub page and serving to highlight a specific point of interest. Starting with one item and growing throughout the day as related articles were published, the list needed to be a rectangular grid rather than a single column, and never have “leftover” items in the last row. And no matter how many child elements it contained at any given moment, it had to stay tidy and neat because it would display above the fold. Each item would be more or less square, with the first item set at 100% width, the second two at 50%, and all subsequent items 33% and arranged in rows of three. My simple list suddenly wasn’t so simple.

Not everyone wants a generic grid or stack of identical items—there’s something nice about selective prominence, grouped elements, and graceful line endings. These styles can be hardcoded if you know the list will always be an exact length, but it becomes more of a challenge when the length can change. How could I keep that last row tidy when there were fewer than three items?

Various arrangements of list items that do and do not break the planned layout in the bottom row.
Our intended layout would break visually as more items were added to the list.

When it came to actually building the thing, I realized that knowing the length of the list wasn’t very helpful. Having loved Heydon Pickering’s excellent article on quantity queries for CSS, I assumed I could find out the length of the list using QQs, then style it accordingly and all would be fine.

But since my list could be any length, I’d need an infinite number of QQs to meet the requirements! I couldn’t have a QQ for every eventuality. Plus, there were rumors a “Load More” button might be added down the road, letting users dynamically inject another 10 or so items. I needed a different solution.

After a minor meltdown, I asked myself, What would Lea Verou do? Well, not panicking would be a good start. Also, it would help to simplify and identify the underlying requirements. Since the list would fundamentally comprise rows of three, I needed to know the remainder from mod 3.

The “mod” query

Being able to select and style elements by the number of siblings is great, but there’s more to this than mere length. In this case, it would be much better to know if my list is divisible by a certain number rather than how long it is.

Unfortunately, there isn’t a native mod query in CSS, but we can create one by combining two selectors: :nth-child(3n) (aka the “modulo” selector) and the :first-child selector.

The following query selects everything if the list is divisible by three:

li:nth-last-child(3n):first-child,
li:nth-last-child(3n):first-child ~ li { 
 … selects everything in a list divisible by three … 
}
Four rows of list items (cats in boxes). The top and bottom rows are selected (full color, not grayed out) because each is divisible by 3.
Only those rows divisible by three are selected. See the Pen Using CSS Mod Queries with Range Selectors: Fig 2 by Patrick (@clanceyp) on CodePen. Cat image via Paper Bird Publishing.

Let’s talk through that code. (I use li for “list item” in the examples.)

Select all following siblings:

li:nth-last-child(3n):first-child ~ li

The first child (first li in the list, in this case):

li:nth-last-child(3n):first-child ~ li

Every third item starting from the end of the list:

li:nth-last-child(3n):first-child ~ li

That combination basically means if the first child is 3n from the end, select all of its siblings.

The query selects all siblings of the first item, but doesn’t include the first item itself, so we need to add a selector for it separately.

li:nth-last-child(3n):first-child,
li:nth-last-child(3n):first-child ~ li { 
… styles for list items in a list divisible by 3 …  
}

Check out the demo and give it a try!

What about remainders?

With my mod query, I can select all the items in a list if the list is divisible by three, but I’ll need to apply different styles if there are remainders. (In the case of remainder 1, I’ll just need to count back in the CSS from the second-to-last element, instead of the last. This can be achieved by simply adding +1 to the query.)

li:nth-last-child(3n+1):first-child,
li:nth-last-child(3n+1):first-child ~ li { 
… styles for elements in list length, mod 3 remainder = 1 …  
}

Ditto for remainder 2—I just add +2 to the query.

li:nth-last-child(3n+2):first-child,
li:nth-last-child(3n+2):first-child ~ li { 
… styles for elements in list length, mod 3 remainder = 2 …  
} 

Creating a range selector

Now I have a way to determine if the list length is divisible by any given number, with or without remainders, but I still need to select a range. As with mod query, there isn’t a native CSS range selector, but we can create one by combining two selectors: :nth-child(n) (i.e., “everything above”) and :nth-child(-n) (i.e., “everything below”).

This allows us to select items 3 to 5, inclusive:

li:nth-child(n+3):nth-child(-n+5){ 
... styles for items 3 to 5 inclusive ...
}
A row of six list items (graphics of cats in boxes). The two on the left are grayed out, followed by three “selected” cats in full color, and the one on the right is grayed out.
We’ve selected a range: cats 3, 4, and 5.

True, that could just as easily be achieved with simple :nth-child(n) syntax and targeting the item positions directly—li:nth-child(3), li:nth-child(4), li:nth-child(5){ ... }—but defining a start and end to a range is obviously much more versatile. Let’s quickly unpack the selector to see what it’s doing.

Selects all the items up to and including the fifth item:

li:nth-child(n+3):nth-child(-n+5){ … }

Selects all the items from the third item onwards:

li:nth-child(n+3):nth-child(-n+5){ … }

Combining the two—li:nth-child(n+3):nth-child(-n+5)—creates a range selector.

If we look at an example, we might have a product grid where the list items contain an image, title, and description. Let’s say the product image speaks for itself, so in the first row we promote the image and hide all the text. With the second and third row, we display the title and image as a thumbnail, while in subsequent rows we hide the image and show the title and description on a single line.

A grid of three cat graphics in a top row, then two rows of  blocks each comprising a cat graphic and a product title, then four rows each listing text for product title and  product details.
A product grid of our cats. We have standalone graphics in the top row, small graphics plus product titles in the second and third rows, and then we lose the graphics and only show text for all rows after that. See the Pen Using CSS Mod Queries with Range Selectors: Fig 4 by Patrick (@clanceyp) on CodePen.

By using the range selector, we can select the first three, the fourth through ninth, and the 10th onwards. This allows us to change the ranges at different breakpoints in the CSS so we can keep our product grid nice and responsive.

Notes on SCSS mixins

Since I was using a CSS preprocessor, I simplified my code by using preprocessor functions; these are SCSS mixins for creating range selectors and mod queries.

// range selector mixin
@mixin select-range($start, $end){
  &:nth-child(n+#{$start}):nth-child(-n+#{$end}){
   @content;
   }
}
// mod query mixin
@mixin mod-list($mod, $remainder){
  &:nth-last-child(#{$mod}n+#{$remainder}):first-child,
  &:nth-last-child(#{$mod}n+#{$remainder}):first-child ~ li {
    @content;
    }
}

Then in my code I could nest the mixins.

li {
@include mod-list(3, 0){
  @include select-range(3, 5){
    // styles for items 3 to 5 in a list mod 3 remainder = 0
    }
  }
}

Which is, if nothing else, much easier to read!

Putting it all together

So now that I have a little arsenal of tools to help me deal with mods, ranges, and ranges within mods, I can break away from standard-implementation fixed length or fixed-layout lists. Creative use of mod queries and range selectors lets me apply styles to change the layout of elements.

Getting back to the original requirement—getting my list to behave—it became clear that if I styled the list assuming it was a multiple of three, then there would only be two other use cases to support:

  • Mod 3, remainder 1
  • Mod 3, remainder 2

If there was one remaining item, I’d make the second row take three items (instead of the default two), but if the remainder was 2, I could make the third row take two items (with the fourth and fifth items at 50%).

In the end, I didn’t need numerous queries at all, and the ones I did need were actually quite simple.

There was one special case: What if the list only contained two elements?

That was solved with a query to select the second item when it’s also the last child.

li:nth-child(2):last-child { 
... styles for the last item if it’s also the second item ...
}

The queries ultimately weren’t as hard as I’d expected; I just needed to combine the mod and range selectors.

li:nth-last-child(3n):first-child /* mod query */ 
~ li:nth-child(n+3):nth-child(-n+5){ /* range selector */
... styles for 3rd to 5th elements, in a list divisible by 3 ...
}

Altogether, my CSS looked something like this in the end:

/* 
  default settings for list (when its mod 3 remainder 0)
  list items are 33% wide
  except; the first item is 100% 
          the second and third are 50%
*/
li {
  width: 33.33%;
}
li:first-child {
  width: 100%;
}
/* range selector for 2nd and 3rd */
li:nth-child(n+2):nth-child(-n+3){
  width: 50%;
}
/* overrides */
/* mod query override, check for mod 3 remainder = 1 */  
li:nth-last-child(3n+1):first-child ~ li:nth-child(n+2):nth-child(-n+3) {
  width: 33.33%; /* override default 50% width for 2nd and 3rd items */
}
/* mod query override, check for mod 3 remainder = 2 */ 
li:nth-last-child(3n+2):first-child ~ li:nth-child(n+4):nth-child(-n+5) {
  width: 50%; /* override default 33% width for 4th and 5th items */
}
/* special case, list contains only two items */
li:nth-child(2):last-child {
  margin-left: 25%;
}

Experience for yourself (and a note on browser support)

The mod queries and range selectors used in this article rely on the CSS3 selectors, so they will work in all modern browsers that support CSS3, including Internet Explorer 9 and above (but remember, IE will expect a valid doctype).

I created a small mod query generator that you can use to experiment with mod queries.

When I first came across QQs, I thought they were great and interesting but largely theoretical, without many practical real-world use cases. However, with mobile usage outstripping desktop, and responsive design now the norm, the need to display lists, target parts of lists depending on the length/mod, and display lists differently at different breakpoints has become much more common. This really brings the practical application of QQs into focus, and I’m finding more than ever that they are an essential part of the UI developer’s toolkit.

Additional resources

Read more

Leave a Reply

Your email address will not be published. Required fields are marked *