CSS Selectors for the Entire Last Row of a Dynamic Grid

A calendar showing the month of May
James Steinbach

UX Developer

James Steinbach

Allow me to begin by describing a recent layout problem I needed to solve. I was working with a dynamic grid of items: the number of items in the grid was variable (provided by an API payload, not a predictable multiple of the column count). The grid items used margin-bottom to create vertical space (browser-support requirements didn’t allow CSS Grid and row-gap), so items in the bottom row of the grid needed that margin removed (or else there would have been too much space below the grid container). For example, we might be styling a grid of related products with a variable number. A calendar layout would match this use case too, since the final row of “days” would vary by month.

I needed a way to select elements in the final row of that grid. Here’s the CSS selector set I used to target elements in the last row.

Finding Children that Could Be in the Final Row

I started by finding all the elements that could be in the last row. The :nth-last-child() selector makes this possible. The :nth-last-child() behaves just like :nth-child() but calculates all element positions from the end of the parent element, not the beginning.

For example, this CSS finds the third element from the end of its parent:

.child:nth-last-child(3) { }

This CSS targets every even-numbered child element, starting at the end:

.child:nth-last-child(even) { }

This CSS selects the last four children in the parent element:

.child:nth-last-child(-n + 4) { }

That’s the pattern we need to select the elements that could be in the final visible row. Here’s how that would work with a responsive grid that expands from two columns to four columns to six columns inside media queries:

/* Under 500px, select the last 2 elements */
@media (max-width: 499px) {
  .child:nth-last-child(-n + 2) { }
}
/* Between 500px - 799px, select the last 4 elements */
@media (min-width: 499px) and (max-width: 799px) {
  .child:nth-last-child(-n + 4) { }
}
/* At 800px & above, select the last 6 elements */
@media (min-width: 800px) {
	.child:nth-last-child(-n + 6) { }
}

With these selectors, we’ve targeted any element that could be in that last row of the grid. We still have a small problem however. If the data doesn’t provide enough items to fill the final row, we’ve selected too many items. For example, if the payload from the API returned nine items, and our grid was six columns wide, we’d only have three items in the last row, but this CSS selector would select the final six items. In just a moment, we’ll add another selector that allows us to select only the final three items in that example.

Notes: I used strictly limited min-width & max-width media queries because :nth-child() adds specificity that we don’t want to have to override at a new breakpoint.

For more information about these selectors, read our DockYard introduction to these pseudo-classes, or MDN’s :nth-child() and :nth-last-child() docs.

Finding the First Child in Every Row

We can chain multiple pseudo-selectors onto a single CSS selector. This will allow us to find the element that’s possibly in both the final row and the first child of a row. We’ve already got the elements that could be in the final row. Now let’s select any element that’s first in its row. (For this example, we’re assuming six elements per row.)

The :nth-child() selector gives us access to every element that’s first in its row, as long as we know the number of columns. (In this example, we’re using six columns.)

.child:nth-child(6n + 1) { }

Inside the :nth-child() function, 6n finds every element whose position is divisible by six (0, 6, 12, so on). That’s the last element in every row, so we all + 1 and the selector now targets the next element position (1, 7, 13, so on). We’ve now selected the first element in every row:

  • item 1 == (6 * 0) + 1
  • item 7 == (6 * 1) + 1
  • item 13 == (6 * 2) + 1
  • item 19 == (6 * 3) + 1

We combine :nth-child(6n + 1) with :nth-last-child(-n + 6), and now we have a single line of CSS that always selects the first element in the last row of content:

.child:nth-child(6n + 1):nth-last-child(-n + 6)

There’s one more small adjustment we need to make. We’ve selected the first element in the last row, but we also need all the other elements in the last row. CSS’s ~ selector (general sibling combinator) lets us select all of those elements:

/* first row item & in the last row */
.child:nth-child(6n + 1):nth-last-child(-n + 6),
/* all the remaining children */
.child:nth-child(6n + 1):nth-last-child(-n + 6) ~ .child { }

The first line in that selector finds just the element that is both “the first child in its row” and “within the last row count of items.” The second line finds all the elements that follow the first element in the final row.

For any grid column count, we can substitute the column count for 6, and we can also wrap it in media queries for responsive selectors. Here’s our responsive CSS from earlier, upgraded with our awesome selector:

@media (max-width: 499px) {
	.child:nth-child(2n + 1):nth-last-child(-n + 2),
	.child:nth-child(2n + 1):nth-last-child(-n + 2) ~ .child { }
}
@media (min-width: 500px) and (max-width: 799px) {
	.child:nth-child(4n + 1):nth-last-child(-n + 4),
	.child:nth-child(4n + 1):nth-last-child(-n + 4) ~ .child { }
}
@media (min-width: 800px) {
	.child:nth-child(6n + 1):nth-last-child(-n + 6),
	.child:nth-child(6n + 1):nth-last-child(-n + 6) ~ .child { }
}

Here’s a CodePen showing this CSS in action:

Note: If you’re into Sass or a similar preprocessor, this repetitive code could be DRYed out with a mixin.

A Better Way: Grid

Of course, it’s worth pointing out that this exact problem would be a non-issue with CSS Grid. In CSS Grid, we’d use the row-gap property to create space only between rows: there’d be no need to find the final row items to override any spacing or layout CSS.

DockYard is a digital product agency offering exceptional user experience, design, full stack engineering, web app development, custom software, Ember, Elixir, and Phoenix services, consulting, and training. With a nationwide staff, we’ve got consultants in key markets across the U.S., including Portland, Los Angeles, Salt Lake City, Minneapolis, Miami, Washington D.C., and Boston.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box