Alternate Titles: Why CSS3 Selectors are Awesome or Counting Elements in CSS3 or Faking Flexbox or CSS3 witchcraft

Intro

CSS3 selectors are just plain awesome. With the new selectors and a little critical thinking, there's so much you can do. Take for example the oh-so-popular dynamic grid. There are many ways to achieve grid layouts, some of the up and coming best practices include the CSS Grid specification and Flexbox. However neither grid nor flexbox layouts have enough browser support for many companies. So what are we to do?

How about using CSS3 selectors to count our cells and then style them appropriately. Here's an example:

Counting Cells with CSS???

That's right. Thanks to CSS3's :nth-child() family of selectors (which have pretty good adoption) we can count exactly how many children a container has. In it's most basic form, that would look something like:

  .grid-element:nth-child(3):last-child
{
    /* The third element is also the last. That means there are 3 elements. */
}

This is a good start, but it's too specific. This rule counts if there are 3 and excactly 3 elements. The rule also can only be used to style the last element in the grid. We'll deal with this last problem first

Hi I'm DIV this is my brother DIV and that's my other brother DIV

Enter the General Sibling Selector (~). This selector, as the name indicates, lets us style the siblings of an element. Given the nature of CSS however, it only styles siblings that come after the element that matches the preceding selector. That means if we tacked it onto our previous rule to get something like:

  .grid-element:nth-child(3):last-child ~ .grid-element`

Nothing would happen since the first part of that selector has already matched the last element, and it has no siblings that follow it.

Counting Backwards

To fix this problem and help the general sibling selector to do it's job we need to match the first of the 3 grid elements. We can do this by counting backwards so-to-speak. If we make our rule look like:

  .grid-element:first-child:nth-last-child(3)
{
    /* If the first element is also the third from last element, then there are three elements*/
}

What we've been able to do now is write a rule that still counts elements, but gives us a match to the first element in the grid. That means we can now use the sibling selector to expand the rule to all other grid elements.

  .grid-element:first-child:nth-last-child(3), /* This will match the first element when there are 3 */
.grid-element:first-child:nth-last-child(3) ~ .grid-element /* This matches all the elements following the first when there are 3 */
{
  width: 33.33%; /* We have a grid with 3 elements, set width to 33% */
}

So now we can count the number of elements in a grid, and select all of the grid elements for styling. But does that mean that we're stuck writing as many rules as there are elements? That's not very flexible.

Generalizing All The Things

When making a grid, we don't actually care how many elements the grid has, but rather how many columns it should be presented in. So can we rework our cell-counting rules to pick an optimal number of columns based on the number of elements?

The N in :nth-Child()

Thanks to the :nth-child selector's ability to select every N objects we can not only count the total number of elements, but also determine if the number of elements is divisible by a given factor. Let's take our CSS rule from above and expand it to determine if the elements are best split into 2 or 3 columns.

  .grid-element:first-child:last-child
{
    /* There is only one element */
  width: 100%:
}
.grid-element:first-child:nth-last-child(2n), /* The number of elements is divisible by 2 */
.grid-element:first-child:nth-last-child(2n) ~ .grid-element
{
    /* Use a 2 column grid since the number of elements is divisible by 2 */
  width: 50%; 
}
.grid-element:first-child:nth-last-child(3n), /* The number of elements is divisible by 3 */
.grid-element:first-child:nth-last-child(3n) ~ .grid-element
{
    /* Use a 3 column grid since the number of elements is divisible by 3 */
  width: 33.33%; 
}

Now we can style the elements as a 1, 2 or 3 column grid depending on how they split up most efficiently. And it only took 3 rules! But there's a problem. Not every number is divisible by 2 or 3. For example 5, 7, 11, and 13 cannot be split into 2 or 3 columns evenly.

Fighting the Primes

The easiest way to deal with these numbers is to just use an exception to our divisible by 2 rule. Any number of elements that isn't divisible by 2 will have exactly one too many elements. I don't know a better way to word that, but here's what I mean: 5 can't be evenly divided by 2, but 5-1=4 and 4 can. The same goes for 7, 11, 13, 17, etc etc. With that mathematical principle in mind we can attack all of our "prime" numbers in one fell swoop. Namely these 2 rules:

  .grid-element:first-child:nth-last-child(2n+1), /* The number of elements is one too many to be divisible by 2 */
.grid-element:first-child:nth-last-child(2n+1) ~ .grid-element
{
    /* Use a 2 column grid for most of the elements */
  width: 50%; 
}
.grid-element:first-child:nth-last-child(2n+1) ~ .grid-element:last-child {
    /* This is our "leftover" element so make it 2 columns wide */
  width: 100%;
}

With two more rules, a total of 5, we have now created a 3 column grid that can accomodate any number of elements.

Outro

Using nothing but widely adopted CSS3 selectors and 5 rules we've been able to create a responsive grid that will dynamically resize it's elements to fill the available width and have 1-3 columns. The rules described in the article still leave a little bit to be desired however. It's left as an exercise for the reader to try the following:

  • Place(or change) the 2n+1 rules in the stylesheet so they don't conflict with the (3n) rule (see pen above)
  • Never orphan an element on a row by itself. e.g. If there are 7 elements split them into 2,2,3, not 2,2,2,1 or 3,3,1 columns. (see pen above)
  • Change the rules to use the minimal number of rows. e.g. For 5 elements don't split into 2,2,1 but rather 3,2 (see pen above)
  • Try reversing the logic to put the wider elements at the top
  • Expand the rules to cover 4 or 5 column grids

3,747 9 46