From inline-block to CSS Grid

,

I rewrote an inline-block based layout in CSS Grid. This is a detailed walk-through.

I rewrote my homepage navigation using CSS Grid. Here’s what the navigation should look like:

a grid of 4x4 navigation cards, with one large card
Bad centering. If the last row isn’t filled with cards, the cards should stay tucked to the left.

Inline-block

The previous solution was based on inline-block, which does the job across all browsers, but at a price:

Here’s the inline-block CSS for a given set of grid dimensions:


.nav{
    max-width: 1232px;
    margin: auto;
}

.nav_item{
    display: inline-block;
    vertical-align: top;
    width: 300px;
    height: 150px;
    margin: 4px;
}

@media screen and (min-width: 633px){
    .nav_item.large{
        width: 608px;
    }
}

@media screen and (max-width: 1264px){
    .nav{max-width: 924px;}
}

@media screen and (max-width: 964px){
    .nav{max-width: 616px;}
}

@media screen and (max-width: 632px){
    .nav{max-width: 308px;}
}

Notice the multiple hard coded numbers in the code. Using inline-block gets the job done but requires significant maintenance.

Another drawback of inline-block is that it’s sensitive to white space in markup. White spaces, including line breaks, translate into margins between elements. My cards in HTML had to be written like this:


<a class="nav_item" href="/blog/">
    <h1>Notes</h1>
</a><a class="nav_item" href="/backspace-mirror/">
    <h1>Backspace Mirror</h1>
</a>

Notice how the a elements are kept adjacent in the markup to avoid white space. This makes the markup less readable, and manipulation of blocks of code more tedious.

CSS Grid

Let’s start fresh and rewrite everything in CSS Grid.

Here’s the markup:


<nav class="nav">
    <a class="nav_item large" href=""></a>
    <a class="nav_item" href=""></a>
    <a class="nav_item" href=""></a>
    <!-- ... -->
</nav>

Here’s the CSS:


.nav{
    display: grid;
    max-width: 1224px;
    margin: auto;
    padding: 8px;
    grid-template-columns: repeat(auto-fit, 300px);
    grid-auto-rows: minmax(150px, auto);
    grid-gap: 8px;
    justify-content: center;
}

@media screen and (min-width: 641px){
    .nav_item.large{
        grid-column: span 2;
    }
}

grid-template-columns: repeat(auto-fit, 300px) means that the grid must fit as many columns of exactly 300 pixels as possible. This makes the grid automatically responsive. More or less columns will be fitted depending on the container’s width.

grid-auto-rows: minmax(150px, auto) means that each row is at least 150 pixels tall, and can stretch to accommodate taller content.

grid-gap: 8px means that there must be 8 pixels between the grid items. Note that grip-gap applies only to the inner gutter. No gap will be added between the grid items and the edges of the grid. That’s why a padding: 8px is added to the grid container.

max-width: 1224px tells the grid to stop at 4 columns. That value is equal to 4*300px + 3*8px (the 3 inner gaps) = 1224px.

justify-center: center tells the grid to center its items along the row axis, which means horizontally centered in this example. Note that the automatic flow of CSS Grid maintains the left-to-right placement of the items in a row. The last items won’t be centered relatively to the grid if there aren’t enough items to fill a row:

centered grid of 4x4 elements
Justify-content: center

Finally, we add the large card code for bigger screens inside a media query. The value span 2 of the property grid-column tells the large card to span across 2 column, no matter the width of the columns or the gap between them.

And we’re done! This setup is enough to get the main grid up and running on all screen sizes!

A step further

Let’s take this a step further and rewrite all the homepage in CSS Grid.

Here’s the layout I’m aiming for:

a header, a grid of 4x4 cards, and a footer

Here’s the HTML:


<main class="main">
    <header class="header"></header>
    <nav class="nav">
        <a class="nav_item large" href=""></a>
        <a class="nav_item" href=""></a>
        <a class="nav_item" href=""></a>
        <!-- ... -->
    </nav>
    <footer class="footer"></footer>
</main>

Here’s the CSS:


.main{
    display: grid;
    padding: 0 8px;
    grid-template-columns: repeat(auto-fit, 300px);
    justify-content: center;
    grid-gap: 8px;
}

.header{
    grid-column: 1/5;
}

.nav{
    grid-column: 1/5;
    display: grid;
    grid-template-columns: repeat(auto-fit, 300px);
    grid-auto-rows: minmax(150px, auto);
    grid-gap: 8px;
    justify-content: center;
}

.footer{
    grid-column: 1/5;
}

@media screen and (min-width: 641px){
    .nav_item.large{
        grid-column: span 2;
    }
}

An interesting behavior is emerging from this setup: did you notice that we no longer specify a max-width or a margin: auto on any container? Yet, the grid stays centered with a maximum of 4 columns as intended. This is the result of the grid-column property.

grid-column: 1 / 5 means that the element must span horizontally from the first vertical line of the grid to the fifth vertical line of the grid. Those numbers were confusing to me until I accepted that they represent lines, not columns:

CSS Grid column lines
Column lines

The first line is the left-most edge of the grid. The fifth line is the line after the fourth column. So 1/5 translates to 4 columns. Therefore the grid will accommodate 4 horizontal cards at most, and then automatically reflow on a new row. In conjunction with justify-content: center, this effectively removes the need to specify the container’s width! That’s a nice emergent behavior!

Benefits

What did I get from rewriting the homepage in CSS Grid?

Compared to the inline-block implementation, I get the same visual layout with fewer lines of code and quicker iteration on the grid parameters.

I also get new options that I couldn’t consider before.

For example, the fact that the large card was defined as twice as large as the regular ones was a product of the limitations of the previous code. I couldn’t get a card that was twice as tall without significant hacking. But with CSS Grid, I could write:


.nav_item.tall{
    grid-row: span 2;
}

And the card would be twice as tall as the others:

a grid of 4x4 cards, with one card spanning across 2 rows

Another interesting case: what if I have 2 large cards, but the screen can only accommodate 3 columns? I would have a gap in the grid like this:

a grid of 3x3 cards, with 2 consecutive cards spanning across 2 columns, leaving a gap between them

But if I set grid-auto-flow: dense on the grid container, I get this:

a grid of 3x3 cards, with 2 consecutive cards spanning across 2 columns, and a card filling the gap between them

CSS Grid automatically fills in the available space by reflowing the cards. Nice!

Here’s another feature. We can write:


.nav_item.nested{
    grid-column: 2 / -2;
    grid-row: span 2;
}

And we get this large nested card:

CSS Grid column lines

grid-column: 2 / -2 tells the element to start after the second line of the grid, and to end before the second line of the grid, counting from the right! The negative value -2 is indeed the second line of the grid, but from the right edge of the grid.

This means that the element always has one column on its left and on its right, no matter the total number of columns. If the screen cannot accommodate a minimum of 3 columns in total, the auto placement algorithm will kick in, and we can use media queries to be more specific.

Browser support

The downside of CSS Grid is browser support. All major western browsers support CSS Grid. But older versions of these browsers do not. CSS Grid doesn’t work on Internet Explorer or Mobile Safari on iOS 10.2 and below.

The layout I implemented is grid-dependent. There needs to be a full alternative for other browsers.

I see 3 options:

Since I decided to make CSS Grid the default implementation, I’ll add legacy code only when CSS Grid is not supported, and drop legacy code as soon as possible. Therefore I need to test for non support of CSS Grid. Feature queries are not supported in Internet Explorer, so I’ll use Javascript.

Here’s the code:


if(!('grid' in document.body.style)){
    var fallback = '<link rel="stylesheet" type="text/css" href="fallback.css">';
    document.head.insertAdjacentHTML('beforeend', fallback);
}

We test for CSS Grid support programmatically, and if it’s not supported, we include a fallback stylesheet. Which CSS fallback solution to use depends on the project and audience. In my case, I fall back to Flexbox combined with media queries.