DEV Community

Chen Hui Jing
Chen Hui Jing

Posted on

Basic grid layout with fallbacks using feature queries

I've been using CSS grid (which henceforth will be referred to as Grid) for quite a bit now, and although I often talk about how we can use grid to make all kinds of creative layouts, I'm fully aware that a basic grid is still a design pattern that is very much in demand on the web.

Many conversations with fellow developers about using Grid involve the dreaded โ€œbutโ€ statement, i.e. โ€œbut what about older browsers? I have to support *INSERT_RANDOM_OLD_BROWSER_HERE*โ€. And it's totally understandable, because I also realised that not enough people know about the magic that is Feature Queries.

This post got too long again ๐Ÿ˜Œ, so here's a table of contents, if there's a particular section you're interested in. And source code for the demo is available on GitHub.

Falling back with grace

The @supports rule, AKA feature queries, is a conditional group rule whose condition tests whether the user agent supports CSS property:value pairs. In plain English, it's an if statement to check if the browser supports a particular CSS property.

Here's how support for it looks like right now (as of 19 Dec 2017):
Can I Use? Feature Queries

I can already sense your eyes fixated on the column of red that is Internet Explorer, but stay with me and don't throw in the towel on feature queries yet. How feature queries work is that, for browsers that don't support it, all the styles within the @supports block is totally ignored.

Let's examine this basic example:

main {
  background-color: red;
}

@supports (display:grid) {
  main {
    background-color: green;
  }
}
Enter fullscreen mode Exit fullscreen mode

For browsers that support grid, the background colour of the main element will be green (because the conditional resolves to true), while for browsers that do not support grid, main will have a background colour of red.

The implication of this behaviour means that we can layer on enhanced styles based on the features we want to use and these styles will show up in browsers that support them. But for those that do not, they will get a more basic look that still works anyway. And that will be our approach moving forward.

Websites do NOT need to look the same on every browser.

First principles of CSS: specifications

Before we go into this, I want to emphasise that implementing layout on the web is significantly different than that on any other media. Why? Because our canvas is the browser and the only way to manipulate the layout is through code.

This degree of separation between the canvas and our input means that we must have knowledge of the workings of the browser that will render our code on the screen, a clear understanding of how the code we write will be interpreted.

A little more than a year ago, I asked the question, How well do you know CSS display? That was the first time I sat down and really read the CSS Display Module Level 3 specification. Layout is quite a large topic and crosses multiple specifications, including the CSS Positioned Layout Module Level 3, CSS Box Alignment Module Level 3, CSS Intrinsic & Extrinsic Sizing Module Level 3 and so on. You get the picture.

The specifications themselves refer to each other liberally, and thus reading one of them can result in your browser tab collection growing exponentially as you get linked to more and more related specifications (or maybe it's just me) ๐Ÿ˜ˆ.

What is this display you speak of?

In case you don't share my interest in reading specifications, let's briefly go through some of the more salient points of the CSS Display specification.

  1. Browsers draw boxes
  2. CSS generates a box tree
  3. Each box represents its corresponding element on the canvas
  4. For each element, CSS generates zero or more boxes based on the element's display property

If you've never read the specification before, I suspect you wouldn't know that the display property defines an element's display type, which is how an element generates boxes. There are 2 basic display types, inner and outer.

The inner display type dictates how child boxes are laid out.

The outer display type dictates how the box itself participates in the layout.

There are additional display values like <display-listitem>, <display-internal>, <display-box> and <display-legacy>.

A formatting context is the environment in which a set of related boxes are laid out, and different formatting contexts will lay out their boxes according to different sets of rules.

We started out with the block formatting context and inline formatting context back in the 1998 CSS2 working draft, but with the addition of more layout options since then, we now also have the table formatting context, flex formatting context, grid formatting context and ruby formatting context.

Block-level elements are elements that generate a block-level principle box, which participate in the block formatting context. Inline-level elements are elements that do not form new blocks of content, and their content is distributed in lines. They generate inline-level boxes, which participate in the inline formatting context.

An inline-block element, however, is not an inline box. It exists in that in-between state because it participates in the inline formatting context as a single opaque box.

Okay, I think that's enough specification talk for now. There's so much more to cover, so I'll probably do this in bits and pieces, for easier digestibility ๐Ÿค“.

Basic, like really basic

I just watched Rachel Andrew's talk at NordicJS the other night and one of the things she covered (and I live-tweeted) was how to fallback gracefully by understanding how the browser handles overridden CSS properties.

Liquid error: internal

I'm going to be using 25 card-style items for the layout demo. Each card will have an image, a title and a subtitle. The title and subtitle text have variations on their lengths, because that's just how real life works. The markup, which will be used for all the layouts demonstrated in this post, looks something like this:

<main class="grid">
    <div class="grid__item">
      <img class="card__img" src="images/image1.jpg">
      <h2>Card title</h2>
      <p>Because the card pattern is soooo popular right now...</p>
    </div>
    <div class="grid__item">
      <img class="card__img" src="images/image2.jpg">
      <h2>Card title</h2>
      <p>Because the card pattern is soooo popular right now, but not all text are made the same</p>
    </div>
    <!-- And repeat the .grid__item another 23 times -->
</main>
Enter fullscreen mode Exit fullscreen mode

So in that vein, let's start off with the most basic of layouts using inline-block.

The inline-block layout

Because I'm a stickler for punishment, this basic layout was tested to be working on IE8 (not further back, as I'm not a masochist). This option assumes that each card is a fixed size (e.g. 20em), and the entire layout will be centred on the viewport.

Inline-block technique
This is actually kinda responsive (on IE8 no less)

The code for this isn't very complicated, as the layout container is set to display: inline-block, centring involves using text-align: center on its parent, in this case, the body element.

body {
  text-align: center;
}

.grid {
  display: inline-block;
}

.grid__item {
  width: 20em;
  display: inline-block;
  vertical-align: top;
  margin: 1em 0.5em;
  text-align: left;
}
Enter fullscreen mode Exit fullscreen mode

This method works reasonably well if you want all the items in your grid to be the same fixed width, and doesn't take much code to do it. You may run into issues if your images are not all the same height though.

Inline-block technique issue
Well, some people might not like this

I'll cover the image handling issue in a different post, but for support that goes back to IE8, we can't use the current markup. Also, if the last row has less items that the total number of columns, they will end up centre-aligned regardless, and maybe you don't want that either.

Okay, let's look at another method that falls under the basic category.

The float layout

Ah, the trusty float. This was the prevailing method for a pretty long time, and I'm sure there are many sites that are still using this technique. Responsive float-based layouts are, for the most part, not simple affairs.

Floats weren't meant for doing page layout to begin with, hence all the trouble with clearing floats, especially when dealing with irregularly sized items within the layout.

And because of the heavy reliance on media queries, a lot of designers just chose to cap the maximum width of the grid, so you didn't have to write a slew of media queries to support larger and larger viewports. I'm just going to forget that, and max out my layout's column count at 5 ๐Ÿคท.

Float technique
Flexi-floaty layout of cards

Just wanted to add that, if we wanted to make the inline-block layout items take up the width of the viewport, we too can chuck in the same slew of media queries instead of using a fixed width.

Okay, the following code is going to be loooooong...

.clearfix::after {
  content: '';
  display: table;
  clear: both;
}

.grid__item {
  float: left;
  padding: 0.5em;
  width: 100%;
}

@media screen and (min-width: 480px) {
  .grid__item {
    width: 50%;
  }

  .grid__item:nth-child(2n+1) {
    clear: left;
  }
}

@media screen and (min-width: 768px) {
  .grid__item {
    width: 33.333%;
  }

  .grid__item:nth-child(2n+1) {
    clear: none;
  }

  .grid__item:nth-child(3n+1) {
    clear: left;
  }
}

@media screen and (min-width: 1024px) {
  .grid__item {
    width: 25%;  
  }

  .grid__item:nth-child(3n+1) {
    clear: none;
  }

  .grid__item:nth-child(4n+1) {
    clear: left;
  }
}

@media screen and (min-width: 1280px) {
  .grid__item {
    width: 20%;  
  }

  .grid__item:nth-child(4n+1) {
    clear: none;
  }

  .grid__item:nth-child(5n+1) {
    clear: left;
  } 
}
Enter fullscreen mode Exit fullscreen mode

I also cheated and added a .clearfix class to the layout container, because you can't have a float-based layout without clearing it. You just can't. Sorry. But regardless of whether the width of the layout item is fixed or flexible, the nth-child selectors for activating and removing clears are still required.

Because of that, this particular style of writing floats doesn't work on IE8. Only IE9 and above. To negate the nth-child support issue, we would need additional wrappers around the number of items per viewport. As such, I will admit to rejecting multiple proposals by my designers of โ€œ4-3-2-1โ€ responsive columns in the past.

Fallback technique winner: inline-block

Given this basic layout is meant to be the basic fallback, I'd go with the inline-block technique, because it's much less code, and still looks reasonably decent. And remember that:

Websites do NOT need to look the same on every browser.

Bedgy, that's like, basic and edgy

So this will be our first layer of feature query, with Flexbox. The idea of Flexbox was discussed before 2008, with the first working draft of the specification published in 2009. But the implementation of Flexbox was rather messy.

The trouble was that a number of developers used this yet-to-be-finalised feature in production, so everyone was in a bind when it came to updating the implementation. Well, that time has passed, and currently, Flexbox support is excellent.

Can I Use? Flexbox

A common issue I hear is that it's difficult to create a grid system with Flexbox. The thing about Flexbox is, even though you can make a grid system with it, it isn't the best idea to do so. Flexbox is suited for laying out items in a single dimension, where there isn't a relationship between the rows and columns. Think of it more like a daisy chain of flex children.

Flexbox daisychain
Either in rows or columns

Flex items are laid out and aligned within flex lines, which are those purple lines in the diagram. FYI these diagrams I sort of stole from the wonderful Brenda Storer (I did tell her though) to illustrate this concept. Okay, hers look better than mine but I can't just use hers directly, that would be plagiarism.

.grid {
  display: flex;
  flex-wrap: wrap;
  margin: -1em 0 1em -0.5em;
}

.grid__item {
  padding: 1em 0 0 0.5em;
  flex: 1 0 20em;
}
Enter fullscreen mode Exit fullscreen mode

So keep in mind that Flexbox isn't exactly good at being a proper grid. It doesn't handle gutters natively, hence the use of negative margins to get some space between each layout item. In general, Flexbox holds up pretty well, until we get to the last row of the layout. Unless it happens to end at a neat number, we usually end up with some odd orphan layout item(s).

Flexbox techqniue
Well, we did tell โ€˜em to be flexible

We can resolve this by doing the same thing we did for floats, and that is, introduce a slew of media queries, like so:

.grid__item {
  padding: 1em 0 0 0.5em;
}

@media screen and (min-width: 480px) {
  .grid__item {
    width: 50%;
  }
}

@media screen and (min-width: 768px) {
  .grid__item {
    width: 33.333%;
  }
}

@media screen and (min-width: 1024px) {
  .grid__item {
    width: 25%;  
  }
}

@media screen and (min-width: 1280px) {
  .grid__item {
    width: 20%;  
  } 
}
Enter fullscreen mode Exit fullscreen mode

Another option is to cap the maximum width of each layout item, so the orphaned items don't grow to ridiculous sizes. However, by default, everything will align left, and there may be instances where you have an excess of white space on the right of the layout container.

Flex items on last row

If you're okay with that, awesome. But anecdotally, most people would like to centre things ๐Ÿคท.

A little bit about box alignment

Enter, box alignment.

I know I said no more spec talk but this is pretty important, and I'm only covering a tiny part of it, specifically the part on content distribution, i.e. aligning a box's contents within itself.

There are 2 relevant properties for this purpose, align-content and justify-content, as well as a shorthand place-content. As of time of writing, place-content is only supported in Chrome 59 and above, as well as Firefox (not sure from which version onwards though).

These properties behave slightly differently depending on the container they are applied to, so I'll just focus on flex container behaviour for now. justify-content affects the flex items in each flex line and applies along the main axis. align-content affects the flex lines themselves and applies along the cross-axis.

Why not use horizontal and vertical axis? Because these physical directions may be different depending on the flex direction applied to the flex container, as well as the writing mode of the document, in various configurations. The diagrams below are just a subset of all possible permutations.

Flex directions

For this particular demo, we're sticking with the most common setup of flex-direction: row and writing mode of horizontal-tb. The use case here is to centre the content along the horizontal plane, i.e. the main axis. So the property we want to call upon will be justify-content.

There are a myriad of values available for this property, those for distributing content and those for positioning content. For positioning, we have center, flex-start, flex-end, start and end. For distribution, we have stretch, space-around, space-between and space-evenly.

Those values prefixed with flex- only apply to flex containers, but otherwise they do what they say they will do, in that items will align to the start/centre/end of the container. Unfortunately, there is an IE11 bug for justify-content: center, so maybe we could use space-around instead.

For distribution, stretch is treated the same as flex-start. The difference between the other three is the amount of space between each item. space-between will result in the first and last items of the row being flush with the edges of the container.

space-between value

space-around gives each item an equal amount of space around themselves.

space-around value

space-evenly, which is only supported by Firefox as of time of writing, distributes all the items evenly on the row, so the space between the first and last items and the edge of the container is the same as between items.

space-evenly value

Personally, I will go for the justify-content: space-around option and hence my flexbox code will end up looking like this:

@supports (display:flex) {
  .grid {
    display: flex;
    flex-wrap: wrap;
    margin: -1em 0 1em -0.5em;
    justify-content: space-around;
  }

  .grid__item {
    padding: 1em 0 0 0.5em;
    flex: 1 0 20em;
    max-width: 20em;
    width: auto;
    margin: initial;
  }
}
Enter fullscreen mode Exit fullscreen mode

The width: auto is used to reset the width value applied on the basic layout, and same goes for margin: initial. Now because feature queries are not supported by IE11 and below, none of the code in this block will apply on those browsers. Hence the lack of support for the initial property on those browsers is also a moot point.

It just so happens that the browsers that do support feature queries recognise initial as well, so maybe we lucked out for that one. Except Opera Mini. But I checked to see how it looked, and it really isn't that bad, so I'll just live with the extra margin there. Otherwise, you can set an explicit value for the margin for the flex layout, which is fine too.

Again, it's your call on whether having the orphaned items display differently is acceptable or not. It might be alright if your layout items don't have an image that spans the full width of the item, or it might not. Every context is different.

Websites do NOT need to look the same on every browser.

Why hello, Grid, you revolutionary, you...

There's so much I want to say about Grid, but I will hold it all in and only cover what is necessary to achieve the basic grid layout within the scope of this post.

Grid is the most impressive roll-out of a new CSS feature I have ever seen, largely in part due to its development behind a flag. This meant that developers could test out Grid and provide feedback to the browser vendors yet be deterred from writing production code that wasn't fully baked yet.

The only issue with this approach, which Rachel Andrew has highlighted time and time again, is that developer feedback isn't as enthusiastic as we want it to be.

The CSS working group has put all the working drafts of specification on GitHub and all of us are free to participate in discussions, raise issues and provide feedback. If you spot something tiny, like a typo in the specification, you can submit a pull request. It doesn't take too much effort, honestly.

Grid is different from all the layout techniques we've used in the past because it is the only solution that works from the container in (credit to Rachel Andrew for this concept). It calls for a big picture view of the grid you have in mind, as opposed to sizing each item individually, which may sometimes lead to a โ€œmissing the forest for the treesโ€ situation.

The major benefit here is that we donโ€™t need elements to depend on each other for placement. We can specify their position on x and y axis independent of what is around them.

โ€” Brenda Storer

Grid technique
Now that's good behaviour

Another fantastic functionality that only pertains to Grid (at the moment), is grid-gap. We can now tell the browser that we want gaps between the items in our layout and the browser will figure the math out. And we have this feature because Rachel Andrew pushed for it at CSS Day and specification author, Elika J. Etemad (AKA Fantasai), who was in the audience, wrote it into the specification. Detailed story here.

Anyway, with Grid, we finally have a legit technique for laying out items in a grid. Like Pinocchio finally becoming a real boy. None of that complicated math to determine the best width for each item, no more messing with negative margins for gaps. Just good solid CSS grid code.

@supports (display:grid) {
  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(20em, 1fr));
    grid-gap: 0.5em;
    margin: initial;
  }

  .grid__item {
    padding: initial;
    max-width: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

The basic premise of how grid works involves defining the grid, via the grid-template-columns and grid-template-rows. These properties allow you to define the size of the grid tracks for columns and rows respectively. We can then place items in the grid, either by ourselves or allowing the browser to do it for us.

For a regular basic grid, the browser can do it so much better than us, because, you know, math is more their forte than ours? No seriously, the grid auto-placement algorithm is really awesome.

In this basic grid, we're making use of the repeat() function, and the minmax() function. The repeat() function saves us the trouble of having to repeat the same pattern of columns/rows by hand. It also takes either the auto-fill or auto-fit argument, which means the browser will determine how many columns or rows the grid should have, based on the available space.

minmax() takes 2 arguments, a size range between the <min> and <max> values. It let's us cap the minimum width of a grid item at a fixed/inflexible width (<min>), while the <max> value can be a fixed value, a flex value or determined by content size using min-content or max-content.

grid-template-columns: repeat(auto-fill, minmax(20em, 1fr));
Enter fullscreen mode Exit fullscreen mode

This line tells the browser I want to have as many grid columns as the space given allows that are at least 20ems wide. If there is any extra space available, distribute it evenly amongst all the columns. No extra math, no slew of media queries ๐Ÿ™†.

grid-gap: 0.5em;
Enter fullscreen mode Exit fullscreen mode

Instead of using paddings and margins for gutters between each grid item, I just have to set the grid-gap to whatever value I want the gutters to be. grid-gap is actually a short-hand for <โ€˜grid-row-gapโ€™> <โ€˜grid-column-gapโ€™>?, and takes in 2 arguments. If you only use 1 argument, both properties will take the same value.

Altogether now

The final layout code ends up looking like this, based on my decisions to use the inline-block technique for base browsers, the space-around technique for yes-flex-no-grid browsers and grid for all the new kids.

body {
  text-align: center;
}

.grid {
  display: inline-block;
}

.grid__item {
  width: 20em;
  display: inline-block;
  vertical-align: top;
  margin: 1em 0.5em;
  text-align: left;
}

@supports (display:flex) {
  .grid {
    display: flex;
    flex-wrap: wrap;
    margin: -1em 0 1em -0.5em;
    justify-content: space-around;
  }

  .grid__item {
    padding: 1em 0 0 0.5em;
    flex: 1 0 20em;
    max-width: 20em;
    width: auto;
    margin: initial;
  }
}

@supports (display:grid) {
  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(20em, 1fr));
    grid-gap: 0.5em;
    margin: initial;
  }

  .grid__item {
    padding: initial;
    max-width: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

If you ask me, I don't think this is an excessive amount of code. But of course, you're free to disagree. I hope that this will convince you that progressively enhancing your CSS is a valuable design pattern to add to your repertoire.

Also, start reading the specifications if you haven't done so before. They are the root of how browsers are supposed to behave so if you do find a behaviour that doesn't seem right, it might very well be a browser bug. We can raise those with the respective browser vendors. And voilร ! You've just made the web better ๐ŸŽŠ.

Lastly, and say it with me...

Websites do NOT need to look the same on every browser.

Until the next one! ๐Ÿค—

Useful resources

Originally published at www.chenhuijing.com on September 9, 2017.

Top comments (2)

Collapse
 
geoff profile image
Geoff Davis

Great article!

Websites do NOT need to look the same on every browser.

This is key. I feel like especially now as a lot of language features are popping up faster than old browsers are dying, this needs to be understood. Grid makes things look beautiful, and that cannot always be matched 1-to-1 with something like a float or flexbox based layout.

Also, if you are interested in CSS Grid, you should follow Jen Simmons and Rachel Andrew on twitter; they are both Grid experts, and I believe they both helped write the specification.

Collapse
 
huijing profile image
Chen Hui Jing

Thank you, rest assured I already do follow them. Rachel helped write the specification, while both Jen and Rachel have been strong advocates for their implementation across browsers.