Alert Email External Link Info Last.fm Letterboxd 0.5/5 stars 1/5 stars 1.5/5 stars 2/5 stars 2.5/5 stars 3/5 stars 3.5/5 stars 4/5 stars 4.5/5 stars 5/5 stars RSS Source Topic Twitter
I’m redesigning this site in public! Follow the process step by step at v7.robweychert.com.

Sophisticated Partitioning with CSS Grid

Create compelling grid patterns by harnessing specificity.

Thanks to Tinnitus Tracker’s many browsing options, there are more than 1,000 lists of shows on the site, making the show list the most prevalent design pattern. It was clear from the start that this would be the case, and the design of event listings is something I’ve given a lot of thought as a designer and music fan, so it was the first thing I explored in early sketches and mockups.

Early mockup for a show list design

My initial orderly attempts responded directly to the tabular nature of the data, and they were easy to parse, but their sterility was at odds with their content. The point of the site is to share something I love, and these early ideas were all head and no heart.

A handbill from a particularly amazing month at The Troc, October 1994

Thinking about how to convey the emotional substance of these lists, I remembered the monthly handbills I got in the mail as a teenager from the Trocadero Theatre (aka The Troc) in Philadelphia. They were always a thrill to receive; the bookings tended to be great, and the design intensified the excitement. Each postcard-sized handbill was a photocopied grid of roughly a dozen shows, and each listing within the grid was like a tiny poster. They were dense and a little chaotic, and the shows’ haphazard arrangement made them fun to discover, like, “Oh, and look at this show over here!” The Troc’s handbills were a welcome disruption to suburban malaise, a vital dispatch from a hipper, louder place.

Time for some CSS Grid

In addition to making Tinnitus Tracker a more fun experience, taking inspiration from those handbills is a good opportunity to experiment with CSS Grid, and getting started is pretty easy. Here’s what my show list markup looks like:

<table class="show-list">
  <thead>
    <tr>
      <th class="date">Date</th>
      <th class="artist">Artist(s)</th>
      <th class="location">Location</th>
      <th class="notes">Notes</th>
    </tr>
  </thead>
  <tbody class="shows">
    <tr class="show">...</tr>
    <tr class="show">...</tr>
    <tr class="show">...</tr>
  </tbody>
</table>

And some basic styles to get a simple grid going:

.show-list,
.show-list tr,
.show-list td {
  display: block;
}

.show-list thead {
  display: none;
}

.show-list .shows {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}

.show-list .shows,
.show-list .show {
  border: 1px solid black;
}

Shows are now arranged in a four-column grid.

Okay, I’m off to a good start. This design isn’t going to make anyone break out the air guitar just yet, but it’s already looking a little less stuffy.

I need to tidy up that leftover space at the end of the grid. Specifying the grid to be four columns wide, as I have, means any show list with an amount of shows that’s not evenly divisible by four will have leftover space. Unacceptable! I’ll need a way to signal how many items are in the grid so it can be styled appropriately. An additional class on table.show-list will do nicely. I determine and assign that class with Liquid in a Jekyll include:

{% assign shows = page.posts %}
{% assign divisor = 9 %}

{% for i in (1..9) %}
  {% assign quotient = shows.size | modulo: divisor %}
  {% assign quotient_alt = shows.size | minus: 1 | modulo: divisor %}
  {% if quotient == 0 or quotient_alt == 0 %}
    {% break %}
  {% else %}
    {% assign divisor = divisor | minus: 1 %}
  {% endif %}
{% endfor %}

<table class="show-list div{{ divisor }}">...</table>

If Liquid isn’t your thing, the above logic should be able to be expressed without too much trouble in most other template environments, or even on the client side with JavaScript. Here’s the basic translation: Is the number of shows in the current list evenly divisible by nine? If so, add a class of div9 to table.show-list. If not, is the number of shows evenly divisible by eight? Then make the class name div8 instead. If not, how about seven? Six? And so on. (There’s also a bit in there for handling prime numbers, which we’ll come back to shortly.) Our current example, which has six shows, gets a class of div6:

<table class="show-list div6">...</table>

Now I can write some CSS to support these new divisor classes.

.div2 .shows {
  grid-template-columns: repeat(2, 1fr);
}

.div9 .shows,
.div6 .shows,
.div3 .shows {
  grid-template-columns: repeat(3, 1fr);
}

.div8 .shows,
.div4 .shows {
  grid-template-columns: repeat(4, 1fr);
}

.div5 .shows {
  grid-template-columns: repeat(5, 1fr);
}

With div6 styles applied, there’s no more leftover space!

Time for some partitioning

So far, so good. The shows now fit neatly into the grid, maybe even a little too neatly. A lot more can be done with a grid than just giving each of its items a uniform amount of space. Things start getting fun when more sophisticated partitioning comes into play. How can different combinations of columns and rows make more interesting use of the space?

On very small viewports, I’m pretty much limited to stacking things: The grid is one column wide and each item occupies a single row. But as the available space gets wider, the grid can be broken into two columns, and then three, and then four, with the partitioning possibilities increasing significantly each time.

The 149 possibilities I found for partitioning 2×2, 3×2, and 4×2 grids. I’m not certain this is all of them. If you are a combinatorics wizard who can find more, please let me know, and bring me a stiff drink while you’re at it.

Reader, I may have broken my brain a few times working through many, many grid partitioning possibilities before narrowing down the ones that would best suit my purpose:

Two-, three-, and four-column grids with repeating partition patterns for divisors from two through nine

These grid patterns have three main objectives:

  • Maintain (somewhat) consistent whitespace. Content varies a bit from show to show, so this isn’t an exact science, but the idea is to have a roughly equal amount of whitespace within each grid partition. We don’t want one item’s text floating in an ocean of open space while the item next to it is bursting at the seams. Generally speaking, since my text is centered, wider grid items (two or more columns wide) will have more space on the left and right than narrower items (one column wide). To compensate, the trick is to have narrow items span two rows when they appear next to wide items, giving the narrow items more whitespace at the top and bottom. You may notice that the limited options inherent in the three-column grid have kept me from following this rule religiously, but when it’s doable, it works pretty well.
  • Give the appearance of randomness. Remember the haphazard quality of the Troc handbills I mentioned earlier? I want to have a little bit of that in these grids. To do this systematically, I need only observe the maxim that the more complex a pattern is, the less recognizable it is as a pattern. As an example, note that the three- and four-column div6 partition patterns have 12 items. That means that for grids divisible by six, the pattern won’t repeat until there are more than 12 items. The pattern is obscured by its infrequent repetition, giving the grid the appearance of randomness.
  • Don’t forget about prime numbers. There are a lot of prime numbers out there (evenly divisible only by themselves and one), and if I’m not careful, they’ll yield an unwanted single column of stacked grid items when my divisors (two through nine) can’t accommodate them. The solution to this is two-pronged:
    1. In the Liquid template logic I went over earlier, this third line has prime numbers in mind:

      {% assign quotient_alt = shows.size | minus: 1 | modulo: divisor %}
      
Basically, when a prime is encountered, it will be subtracted by one, and a divisor class (div2 through div9) will be assigned according to the difference. So a grid with 13 items will be treated as if it had 12 items, and will get a class of div6. A grid of 51 will be treated as if it were 50, and get a class of div5.
    2. Items one and seven span the full width of these 12-item div6 partition patterns.

      To ensure that the extra item doesn’t break the grid, the first item in every grid partition pattern spans the grid’s full width, as does each grid item that follows a multiple of the divisor. In a 12-item div6 partition pattern, items one and seven get the full-width treatment. Since a 13th item is item one in the first repetition of that div6 pattern, it spans the width of the grid. When it’s the last item, it won’t leave any unsavory extra space at the end.

Time to put it all together

Now that I’m able to assign divisor classes to my show lists, and I have grid partition patterns drawn up for each of those divisor classes, all that’s left to do is connect the dots between the patterns and the class names. That means reworking and adding a lot of specificity to the CSS I wrote earlier. Most of the heavy lifting will be handled by our friend :nth-child().

To begin, for all of the patterns in each breakpoint, I note the position of each instance of each of the five partition types: 1×1, 1×2, 2×1, 3×1, and 4×1. For example, here are all 20 instances of the 2×1 partition type found in the four-column breakpoint:

  • div3 partition 2 of 3
  • div3 partition 3 of 3
  • div5 partition 4 of 15
  • div5 partition 5 of 15
  • div5 partition 12 of 15
  • div5 partition 15 of 15
  • div6 partition 5 of 12
  • div6 partition 6 of 12
  • div6 partition 11 of 12
  • div6 partition 12 of 12
  • div7 partition 2 of 14
  • div7 partition 3 of 14
  • div8 partition 2 of 16
  • div8 partition 5 of 16
  • div8 partition 12 of 16
  • div8 partition 13 of 16
  • div9 partition 2 of 9
  • div9 partition 5 of 9
  • div9 partition 8 of 9
  • div9 partition 9 of 9

Each of those instances is then translated into a CSS selector using this formula:

.[divisor class] .show:nth-child([total partitions]n + [this partition])

The CSS that results is admittedly a mouthful:

@media screen and (min-width: 55em) { /* four-column breakpoint */

  .show-list {
    grid-template-columns: repeat(4, 1fr);
  }

  /* 2x1 partitions */

  .div3 .show:nth-child(3n+2),    /* div3 partition 02 of 03 */
  .div3 .show:nth-child(3n+3),    /* div3 partition 03 of 03 */
  .div5 .show:nth-child(15n+4),   /* div5 partition 04 of 15 */
  .div5 .show:nth-child(15n+5),   /* div5 partition 05 of 15 */
  .div5 .show:nth-child(15n+12),  /* div5 partition 12 of 15 */
  .div5 .show:nth-child(15n+15),  /* div5 partition 15 of 15 */
  .div6 .show:nth-child(12n+5),   /* div6 partition 05 of 12 */
  .div6 .show:nth-child(12n+6),   /* div6 partition 06 of 12 */
  .div6 .show:nth-child(12n+11),  /* div6 partition 11 of 12 */
  .div6 .show:nth-child(12n+12),  /* div6 partition 12 of 12 */
  .div7 .show:nth-child(14n+2),   /* div7 partition 02 of 14 */
  .div7 .show:nth-child(14n+3),   /* div7 partition 03 of 14 */
  .div8 .show:nth-child(16n+2),   /* div8 partition 02 of 16 */
  .div8 .show:nth-child(16n+5),   /* div8 partition 05 of 16 */
  .div8 .show:nth-child(16n+12),  /* div8 partition 12 of 16 */
  .div8 .show:nth-child(16n+13),  /* div8 partition 13 of 16 */
  .div9 .show:nth-child(9n+2),    /* div9 partition 02 of 09 */
  .div9 .show:nth-child(9n+5),    /* div9 partition 05 of 09 */
  .div9 .show:nth-child(9n+8),    /* div9 partition 08 of 09 */
  .div9 .show:nth-child(9n+9) {   /* div9 partition 09 of 09 */
    grid-column: auto / span 2;
  }

}

I wasn’t joking about specificity! It’s a lot of selectors. With this group and all the others (five partition types across three breakpoints), there are nearly 128 total selectors! This is not the kind of thing that can be designed in the browser. It needs to be carefully planned out before it’s coded. (For more info on how :nth-child() functional notation like 3n+2 works, see Chris Coyier’s excellent post on the subject.) It’s no small amount of work, but in this case, I think the results are worth it:

With the updated div6 styles applied, the grid feels more spontaneous.

The typographic cherry on top

The show list design is nearing completion. The grid patterns now in place, which vary according to breakpoint and number of grid items, give the many show lists across the site a lot of visual variety. The main thing they still need is a typographic shot in the arm. A little bit can go a long way, and in this case I think adding contrast by setting the headliners in a much bigger display typeface is the way to go.

The trouble is, that big display type will occupy its space very differently across partition types: the narrower the space, the more aggressively the lines will break, and some longer words might not even fit. To solve this, each partition type would ideally have its own version of the display face: a condensed or compressed width for single-column partitions, medium widths for the two- and three-column partitions, and a wide width for four-column partitions. I researched a lot of font super families before coming to the conclusion that this was actually a perfect opportunity to start playing with variable fonts. Their current browser support and experimental development mean they’re not quite ready for prime time yet, but Tinnitus Tracker, my esoteric live music diary, is anything but prime time.

By an incredible stroke of luck, Nick Sherman was kind enough to let me beta test an in-progress variable version of Curvature, one of his excellent display faces, which has exactly the right feel for this project.

Nick Sherman’s Curvature

Determining how best to use Curvature in the available space means finding the right balance of typographic size and width, accounting for the viewport and the content.

On the viewport side of things, the show lists always occupy the full width of the page layout, which is in proportion with the viewport width. Therefore, using vw units with font-size will allow the display text to scale proportionally with its container.

As for the content, to ensure that everything can fit, the longest available word is used to test size and width adjustments. Only headliners get the display text treatment, and of the nearly 400 shows on the site, the headliner with the longest word in its name is The Dismemberment Plan. If I can make the headliner type look good in every partition type at every breakpoint without dismembering the word “DISMEMBERMENT,” I’m in good shape.

For each breakpoint, the headliner is given the largest font size that will allow “DISMEMBERMENT” to comfortably fit in the narrowest partitions at Curvature’s narrowest width.

Size is assigned according to breakpoint. The wider the layout, the smaller the vw units:

  • 1-column layout: 8vw
  • 2-column layout: 6vw
  • 3-column layout: 4vw
  • 4-column layout: 3vw

Width is assigned according to partition type, using the font-variation-settings property and the wdth variation axis tag. The wider the partition, the larger the wdth:

  • 1×1, 1×2: 150
  • 2×1: 300
  • 3×1: 600
  • 4×1: 900

If you remember that massive block of CSS selectors from earlier, you remember that targeting the partition types requires a lot of specificity, and targeting the content of those partition types is no different:

@media screen and (min-width: 55em) { /* four-column breakpoint */

  .show-list {
    grid-template-columns: repeat(4, 1fr);
  }

  .headliner {
    font-size: 3vw;
  }

  /* 2x1 partitions */

  .div3 .show:nth-child(3n+2),    /* div3 partition 02 of 03 */
  .div3 .show:nth-child(3n+3),    /* div3 partition 03 of 03 */
  .div5 .show:nth-child(15n+4),   /* div5 partition 04 of 15 */
  .div5 .show:nth-child(15n+5),   /* div5 partition 05 of 15 */
  .div5 .show:nth-child(15n+12),  /* div5 partition 12 of 15 */
  .div5 .show:nth-child(15n+15),  /* div5 partition 15 of 15 */
  .div6 .show:nth-child(12n+5),   /* div6 partition 05 of 12 */
  .div6 .show:nth-child(12n+6),   /* div6 partition 06 of 12 */
  .div6 .show:nth-child(12n+11),  /* div6 partition 11 of 12 */
  .div6 .show:nth-child(12n+12),  /* div6 partition 12 of 12 */
  .div7 .show:nth-child(14n+2),   /* div7 partition 02 of 14 */
  .div7 .show:nth-child(14n+3),   /* div7 partition 03 of 14 */
  .div8 .show:nth-child(16n+2),   /* div8 partition 02 of 16 */
  .div8 .show:nth-child(16n+5),   /* div8 partition 05 of 16 */
  .div8 .show:nth-child(16n+12),  /* div8 partition 12 of 16 */
  .div8 .show:nth-child(16n+13),  /* div8 partition 13 of 16 */
  .div9 .show:nth-child(9n+2),    /* div9 partition 02 of 09 */
  .div9 .show:nth-child(9n+5),    /* div9 partition 05 of 09 */
  .div9 .show:nth-child(9n+8),    /* div9 partition 08 of 09 */
  .div9 .show:nth-child(9n+9) {   /* div9 partition 09 of 09 */
    grid-column: auto / span 2;
  }

  .div3 .show:nth-child(3n+2) .headliner,
  .div3 .show:nth-child(3n+3) .headliner,
  .div5 .show:nth-child(15n+4) .headliner,
  .div5 .show:nth-child(15n+5) .headliner,
  .div5 .show:nth-child(15n+12) .headliner,
  .div5 .show:nth-child(15n+15) .headliner,
  .div6 .show:nth-child(12n+5) .headliner,
  .div6 .show:nth-child(12n+6) .headliner,
  .div6 .show:nth-child(12n+11) .headliner,
  .div6 .show:nth-child(12n+12) .headliner,
  .div7 .show:nth-child(14n+2) .headliner,
  .div7 .show:nth-child(14n+3) .headliner,
  .div8 .show:nth-child(16n+2) .headliner,
  .div8 .show:nth-child(16n+5) .headliner,
  .div8 .show:nth-child(16n+12) .headliner,
  .div8 .show:nth-child(16n+13) .headliner,
  .div9 .show:nth-child(9n+2) .headliner,
  .div9 .show:nth-child(9n+5) .headliner,
  .div9 .show:nth-child(9n+8) .headliner,
  .div9 .show:nth-child(9n+9) .headliner {
    font-variation-settings: "wdth" 900;
  }

}

In development, this could be done more neatly with Sass nesting (and maybe a fancy map/loop combo) but it’s important to stay aware of the output on the client side. It only takes about 20 declarations to handle the grid layout and headliner styles across all breakpoints and partition types, but those declarations contain a grand total of 260 compound selectors. If you’re wary of that much heft devoted to that much complex specificity, you’re right to be. Approaches like this should be used with caution. In my case, the site is something of an experiment with very specific needs, and anyway, the remainder of the site’s styles are fairly simple and I’ve kept the overall CSS pretty lean. The nth-child() complexity doesn’t seem to have a noticeable impact on performance, and once again, I think the results justify the effort:

The final product


Visit Tinnitus Tracker to see the show lists in action! If you incorporate and/or improve upon the techniques I’ve covered in this post and want to share what you’ve done, please do. And keep an eye out for my next post about Tinnitus Tracker, which will go over its approach to date-based, dynamic color.