Playing With Flexbox and Quantity Queries

Ever since reading Haydon Pickering’s piece on quantity queries, I’ve been musing over the possibilities for layout. I decided I to play around with them a bit on this site as it’s been a while since I’ve tweaked the design. Being that I wanted to experiment, I thought this would be a fun time to tuck into Flexbox a bit more as well.

Here’s a brief overview of the project:

The Candidate: My speaking engagements page.
The Challenge: The “Future” list will grow and shrink as I book things, so I never know how many will be there. The “Past” list will also grow, but I am less interested in getting crazy with that.
The Idea: A grid layout that flexes to visually highlight 1-2 upcoming future events and allows the others to flow in at the default grid size. It should be set up to handle everything from a single future event to a dozen or more.

My sketch of the idea.

The markup pattern was pretty simple. It’s just a list of events:

<ul class="listing listing--events">
<li class="listing__item listing__item--1 event event--future">
<!-- content -->
</li>
<!-- lis continue -->
</ul>
view raw markup.html hosted with ❤ by GitHub

With that in place, I got to work.

Single File

To set the stage, I started with some basic Flexbox syntax1 by handling the container and the basic full-width small screen view:

.listing--events {
display: flex;
flex-wrap: wrap;
}
.event {
box-sizing: border-box;
padding: 1.25rem; /* 20px padding */
margin: 0 0 1.25rem 0; /* 20px bottom margin */
flex: 0 0 100%;
}
view raw first-pass.css hosted with ❤ by GitHub

You may be wondering where all of the experimental style rules are. I use Autoprefixer to handle the experimental property inclusion/trans-compilation so I can keep my CSS clean and standards-based.

This simple CSS gives you exactly what you’d expect: a vertical list of events, separated by 20px worth of space.

Two by Two

Next up, I tackled the first breakpoint at 28.75em:

.listing--events {
display: flex;
flex-wrap: wrap;
align-items: stretch;
}
.event {
box-sizing: border-box;
padding: 1.25rem; /* 20px */
margin: 0 0 1.25rem 0; /* 20px v */
flex: 0 0 100%;
}
@media only screen and (min-width: 28.75em) {
/* 20px gap divided by 2 events per row */
.event {
flex: 0 0 calc( 50% - 1.25rem / 2 );
margin-left: 1.25rem; /* 20px < */
}
/* Remove left margin for row starters */
.event:nth-child(odd) {
margin-left: 0;
}
/* Reset margins on "future" events
& remove the correct one */
.event--future:nth-child(odd) {
margin-left: 1.25rem;
}
.event--future:nth-child(even) {
margin-left: 0;
}
/* Quantity Query - when more than 1,
make the first span both columns */
.event--future:nth-last-child(n+1):first-child {
flex: 0 0 100%;
font-size: 1.5em;
margin-left: 0;
}
}
view raw second-pass.css hosted with ❤ by GitHub

In this pass, I set up the event blocks to fill 50% of the parent container (well, 50% minus the 1.25rem gutter between them, using calc()).2 In order to make the children wrap to form rows, I set flex-wrap: wrap on the list (.listing--events). Then, to make the children all equal heights across each row, I set align-items: stretch. The gutter space was achieved via left margins on all events save the row starters (.event:nth-child(odd)).

It’s worth noting that in the full page I have two sets of event listings: one past and one future. The “event” class is used in all instances. The modified “future” class is added to events that have not happened yet.

Then I used a quantity query to select the first future event when there is more than one in the list (line 38) and set it span 100% of the parent width. To keep the gutters accurate, I also swapped where the margins were applied, adding the margin back to .event--future:nth-child(odd) and removing it from .event--future:nth-child(even).

Three’s a Crowd

Finally, I could tackle the third and most complicated layout. Things seemed to get a little wide around 690px, so I set the breakpoint to 43.125em.

.listing--events {
display: flex;
flex-wrap: wrap;
align-items: stretch;
}
.event {
box-sizing: border-box;
padding: 1.25rem; /* 20px */
margin: 0 0 1.25rem 0; /* 20px v */
flex: 0 0 100%;
}
@media only screen and (min-width: 28.75em) {
/* 20px gap divided by 2 events per row */
.event {
flex: 0 0 calc( 50% - 1.25rem / 2 );
margin-left: 1.25rem; /* 20px < */
}
/* Remove left margin for row starters */
.event:nth-child(odd) {
margin-left: 0;
}
/* Reset margins on "future" events
& remove the correct one */
.event--future:nth-child(odd) {
margin-left: 1.25rem;
}
.event--future:nth-child(even) {
margin-left: 0;
}
/* Quantity Query - when more than 1,
make the first span both columns */
.event--future:nth-last-child(n+1):first-child {
flex: 0 0 100%;
font-size: 1.5em;
margin-left: 0;
}
}
@media only screen and (min-width: 43.125em) {
/* 1/3 width with 20px gutter */
.event {
flex: 0 0 calc( 100% / 3 - .875rem );
}
/* Reset margins */
.event:nth-child(even),
.event:nth-child(odd) {
margin-left: 1.25rem;
}
/* Normal Grid margin removal */
.event:nth-child(3n+1) {
margin-left: 0;
}
/* Correct margins for the future events */
.event--future:nth-child(3n+1) {
margin-left: 1.25rem;
}
/* Quantity Query - when more than 2,
make the first 2 go 50% */
.event--future:nth-last-child(n+2):first-child,
.event--future:nth-last-child(n+2):first-child + .event--future {
flex: 0 0 calc( 50% - 1.25rem / 2 );
font-size: 1.5em;
}
/* Quantity + nth for margin removal */
.event--future:nth-last-child(n+2):first-child ~ .event--future:nth-child(3n) {
margin-left: 0;
}
}
view raw third-pass.css hosted with ❤ by GitHub

In this final pass, I used a slightly more complicated calculation to set the width of each child to 1/3 of the parent minus the gutters between them (100% / 3 - 0.875rem).

If you’re paying close attention, you might wonder why the gutter being used in the calculation is 0.875rem rather than the full 1.25rem. Well, the reason is (as best I can surmise) rounding. In order to get the flex width to fill the parent without causing a wrap, 14px (0.875rem) seemed to be the magic number.

It’s worth noting that if I allowed the event to grow (using flex-grow: 1 or its equivalent in the shorthand), the column would fill in perfectly, but the last row would always be filled completely too. You could end up with two events in the last row being 50% wide each or a single event being 100% wide, which I didn’t want. I wanted them all to be equal width with the exception of the first 2. Setting flex as I did allowed that to happen.

I went ahead and reset the standard margins for events as well (on both .event:nth-child(even) and .event:nth-child(odd)). And then I turned off the margins on the first of every group of three events using .event:nth-child(3n+1).

With that in place, I went to work on the future events, resetting the margins there as well. Then I used a quantity query (lines 70-71) to select the first two members when the list is more than 2 and set them to be 50% of the parent width minus the gutter.

To handle the margins in the quantity query instance, I added all the margins back (line 64) and then removed the left margins from the new row starters (line 77).

Ta-da!

And there you have it. In about 80 lines of very generously spaced and commented CSS, we’ve got a flexible grid-based Flexbox layout with visual enhancements injected via quantity queries. I’m sure I’ll continue to tinker, but I’m pretty happy with the results so far.

You can view the final page of course (or watch a video of the interaction), but I also created a distilled demo on Codepen for you to play with.

  1. If you aren’t familiar with Flexbox, CSS Tricks has a great cheatsheet

  2. Interestingly, the support matrices for calc() and Flexbox are pretty well aligned. 


Comments


Webmentions

  1. Perhaps I’m thick, but why not just float them all left and set percentage widths on, say, the first two at 50% and everything else at 33%? (Modulo spacing, of course, and maybe do it differently at different break points.) What’s flexbox buying here?

    | Permalink
  2. That would certainly be a possibility. It would require some additional CSS to hold open the parent by clearing the floats of course. That would have been my traditional approach, but I saw this as a chance for me to play with flexbox (which, until now, I had only used to rearrange element flow on a page).

    | Permalink
  3. Ah, fair enough. I currently use flexbox for vertical centring, to do things in the wrong order, and one time for the whole layout which restacks at different break points just to experiment :-)

    | Permalink
  4. Same question I had. I’m pretty sure if you want the wrapping of flexbox while ignoring the other aspects of it (because it seems that’s what you’re doing– any flexiness you get is from your main-size being a calc-based width), then you could simply declare flex-none (== 0 0 [main-size]) at once for all of your items, and do the same with the width (which flexbox then sees as [main-size]). Then you only have to declare the exceptions to those rules (i.e. your larger items in the top row). Or am I missing something? 

    | Permalink
  5. :-) Sorry, just waking up and in the middle of tilting at Ruby windmills. I’m definitely open to suggestion. This is my first pass at working with Flexbox, so I have a lot to learn I’m sure. Apart from that refactor, I’d love to see any other suggestions you have. If you want to fork the Gist or just email me some thoughts, that would be awesome!

    | Permalink

Links

Likes

Shares