Layout

Responsive Column Layouts with CSS Grid and Container Queries

By Aigars Silkalns Updated 6 min read

Responsive Column Layouts with CSS Grid and Container Queries

The modern way to build responsive columns is CSS Grid with repeat(auto-fit, minmax(…)). It makes the browser choose the column count from the space available, with no media queries, no .first or .last classes and no nth-of-type resets. When columns should follow a component’s width instead of the screen, add a container query.

The original version of this tutorial floated percentage-width boxes and used :nth-of-type(4n+1) to clear the left margin on every row, then rewrote those rules at three breakpoints. That worked in 2012. Today it is about forty lines of CSS you no longer need.

Seven equal-width boxes labelled One to Seven, wrapping into two rows of the same column width
The grid from the rule below with seven items: the last row keeps the same column width as the rows above it.

The one-rule responsive grid

This is the whole technique:

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  gap: 1rem;
}

Read it from the inside out:

  1. minmax(11rem, 1fr) says each column is at least 11rem wide and may grow to share leftover space equally.
  2. repeat(auto-fit, …) creates as many such columns as fit in the container. Per MDN, auto-fit collapses empty tracks to zero after items are placed, so a single row with only a few items stretches to fill the width. auto-fill keeps the empty tracks, which leaves those items at their minimum width with blank space after them. The two keywords only differ when there are fewer items than fit in one row. A wrapped last row keeps the column width with either keyword, because the tracks are shared by every row.
  3. min(100%, 11rem) is a safety net. Without it, a container narrower than 11rem would overflow. With it, the column simply becomes one full-width track.
  4. gap replaces the margin arithmetic. Spacing exists only between tracks, so there is nothing to reset on the first item of a row.

The markup stays flat:

<div class="grid">
  <article class="col">…</article>
  <article class="col">…</article>
  <article class="col">…</article>
</div>

Resize the window and the grid moves from six columns to five, four, and eventually one, with every item equal in width. On a narrow phone the same rule gives a single column:

The same grid at 420 pixels wide, showing cards stacked in one column
The same CSS at phone width: one column, no extra rules.

Fixed column counts when you want them

The old demo switched between four, three and two columns at specific widths. Auto-fit gives you fluid counts, which is usually better, but sometimes a design needs exactly four columns on wide screens. Use a custom property and one media query:

.grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 1), 1fr);
  gap: 1rem;
}
@media (width >= 40rem) { .grid { --cols: 2; } }
@media (width >= 64rem) { .grid { --cols: 4; } }

Range syntax such as width >= 40rem is part of Media Queries Level 4 and reads more clearly than min-width. Use rem rather than pixels, so the layout respects a reader’s default font size.

Columns that follow the component, not the viewport

Media queries measure the screen. A card list placed in a wide main area on one page and a narrow sidebar on another needs to measure its own space. That is what container queries are for. Mark the parent as a query container and write rules against it:

.main {
  container-type: inline-size;
  container-name: cards;
}
.cards {
  display: grid;
  gap: 1rem;
}
@container cards (width >= 30rem) {
  .cards { grid-template-columns: repeat(2, 1fr); }
}
@container cards (width >= 48rem) {
  .cards { grid-template-columns: repeat(3, 1fr); }
}

container-type: inline-size applies inline-size containment, so the container’s width can no longer depend on its children. The query target therefore has to be an ancestor of the element you style, which is why the rule sits on .main and the grid is inside it. Container query units such as cqi (1% of the container’s inline size) let type scale with the component too. In the screenshot below, the card titles use this rule, so they grow with the container:

.card h3 { font-size: calc(1rem + 1cqi); }
The same cards in a 360 pixel container stacked in one column, and in a 1000 pixel container laid out in three columns with larger titles
The same markup in two container widths: one column at 360px, three columns at 1000px.

Container size queries are Baseline Widely available, and so are CSS Grid and flexbox, according to the Web Platform Status data, so you can ship them without a fallback.

Aligning content across cards with subgrid

Equal-width columns are only half the problem. If card titles wrap onto two lines in one card and one line in the next, the body text drifts out of line. Subgrid lets each card borrow row tracks from the parent:

.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 1rem; }
.card {
  display: grid;
  grid-row: span 2;
  grid-template-rows: subgrid;
  gap: 0;
}

Each card has two children, a heading and a body, and they now line up across the row, as the second row of the screenshot shows. Subgrid is Baseline Widely available.

Two rows of three cards: without subgrid the body boxes sit at different heights, with subgrid they align along the bottom
Without subgrid (top) the body boxes follow each title’s height. With subgrid (bottom) they share row tracks and line up.

Grid or flexbox for columns?

Need Use
Equal columns that wrap into rows Grid with auto-fit
Items of different widths that wrap naturally, such as tags or buttons Flexbox with flex-wrap
A single short row should not stretch to fill the width Grid with auto-fill
Content aligned across cards Grid with subgrid
One axis only, such as a toolbar Flexbox

Flexbox decides widths per item, so a half-empty last row stretches or misaligns. Grid decides widths per track, which is why it suits column layouts. When you want items to wrap and grow without a grid, Flexbox with gap and flex: 1 1 11rem is a fair alternative, but the last row will stretch its items, so choose it only when that is acceptable. For running text split into columns, as in a newspaper, use CSS multi-column layout with column-width: 18rem instead of grid. If flexbox is new to you, read Master CSS Flexbox in 5 Simple Steps.

Migrating a float grid

  1. Delete the float, percentage widths, clear and every .first/.last or nth-of-type rule.
  2. Set display: grid on the parent and replace margins with gap.
  3. Choose auto-fit with a minimum width, or a --cols variable for fixed counts.
  4. Remove the width overrides in media queries. Keep only the queries that change --cols.
  5. Replace viewport queries with container queries wherever the component is reused in different-width slots.

The old IE8 polyfills (selectivizr and respond.js) have no place here. Grid is supported by every current browser.

Common mistakes

  • Using auto-fit with a tiny minimum. A 5rem minimum gives you a dozen cramped columns on a desktop. Pick the narrowest width at which your content still reads well.
  • Forgetting min(100%, …). A plain minmax(16rem, 1fr) overflows any container narrower than 16rem, which happens in sidebars and on small phones.
  • Querying the element you style. A container cannot respond to a query about itself. Put container-type on the wrapper and style the children.
  • Mixing percentage widths into grid items. Let the tracks size the items. Set width on a grid item only when you have a specific reason.

Accessibility notes

  • Grid changes visual order only if you tell it to. Avoid order and explicit placement that disagree with the DOM, because keyboard focus follows the source order.
  • Use a sensible minimum column width. A column narrower than a comfortable line length forces text to wrap awkwardly, and the content has to survive 200% zoom, where the grid will drop columns on its own.
  • Do not set fixed heights on cards. Let text wrap when users enlarge it.

Where to go next

For breakpoint decisions in the layouts that still need them, see Responsive Design with CSS3 Media Queries. For more small responsive patterns, 5 Useful CSS Tricks for Responsive Design is a good companion. If you want ready-made grids to adapt, our product card grid and photo gallery snippets show the card patterns in Bootstrap 5.

19 comments from the archive
  1. How about css3-mediaqueries.js instead of respond.js? It should work well with selectivizr.js

  2. Thanks for sharing. Responsive columns work very well in Themify Themes so it’s great to learn how it’s done.

  3. Simply set a left margin on all columns and then reset it on the first element of every column (xn + 1); combine it with a clear: left.
    Also, if you define media queries with a min-width and max-width, there’s no need to ‘reset’ any selectors using the !important rule (which should always only be a last resort).

    Just my 2¢.

  4. Nice article. Been messing around with various options myself for some time now.

    A better backwards compatible solution with inline-block usage:

    Setting a negative margin on the parent container is better supported than nth-child selector.

    In my example I chose to not support mediaqueries for older IE’s, of course this could be easily added.

    1. This is indeed very nice, but you still need nth-of-type() to clear the float for situation like this:

      ps. It doesn’t work on FF.

      1. Hi Nick,

        What do you mean with “you still need nth-of-type() to clear”? You can change the vertical-align to top to let the items align like floated block elements would, except they remain in the row they’re in (a good thing!).

        Which version of FF are you using? I’m using it as well and works fine here :\ I thought I checked all major browser for compatibility.

        Example updated.

  5. Thanks for this helpful post on responsive layouts. I’m studying this now for my second WP Theme.

  6. A plus for examples which can be visited with a banal click. I like how the blog confers the best suggestions on anything

  7. Not an ideal solution because it requires left and right margin which causing the left and right empty space.

  8. Phwoar. This appeals to my nerd sensibilities. Thanks for sharing. Shame as always about the problem of IE, but seems like an acceptable solution.

  9. Great tutorial!
    Would removing all gutters be as a simple as changing margin-left: 3.2% to 0% and adjusting col widths accordingly? Thanks

  10. Pretty awesome, you can also use inline-block; for reflowing grids rather than floats.

Comments are closed on this article.