HTML & CSS

Responsive layout with Flexbox and Grid: which one, when, and a page you can build

Flexbox for one-dimensional rows and columns, Grid for two-dimensional page layout, how to choose between them, and a mobile-first page layout built step by step with no media query until it is actually needed.

By the TechTutorZone editorial team · Published · 6 min read (1,137 words)

Flexbox and Grid are the two modern CSS layout systems, and the most common beginner question is which to use. The short answer: Flexbox arranges things along one line (a row or a column) and is good at distributing space between items whose sizes you do not know; Grid arranges things in rows and columns at once and is good at page-level layout where the structure is known. This tutorial builds a typical page, header, content, sidebar and footer, using each where it fits, and explains the properties as they come up.

Start from the HTML

Layout follows structure, so write the markup first. This uses the semantic elements from the previous tutorial.

<body>
  <header class="site-header">
    <a class="logo" href="/">Site</a>
    <nav><a href="/">Home</a><a href="/posts/">Posts</a><a href="/about/">About</a></nav>
  </header>
  <div class="layout">
    <main>
      <h1>Title</h1>
      <p>Content...</p>
    </main>
    <aside>Sidebar</aside>
  </div>
  <footer>Footer</footer>
</body>

Mobile first means the default styles, with no media query, are the narrow-screen layout: everything in one column. Wider screens then add columns. Doing it this way round keeps the base CSS simple and means small devices download fewer overrides.

Flexbox for the header

The header is one row: logo on the left, links on the right. That is a one-dimensional problem, so Flexbox.

.site-header {
  display: flex;
  align-items: center;       /* vertical centring in the row */
  justify-content: space-between;
  gap: 1rem;
  padding: .75rem 1rem;
}
.site-header nav {
  display: flex;
  gap: .25rem 1rem;          /* row-gap column-gap */
  flex-wrap: wrap;           /* links drop to a second line when tight */
}

Three properties do most of the work:

  • justify-content distributes items along the main axis (horizontal in a row). space-between pushes the first and last to the edges.
  • align-items aligns along the cross axis. center lines up the logo and the links vertically regardless of their heights.
  • gap puts space between items without margins on the items themselves, so there is no stray margin at the ends.

flex-wrap: wrap is the mobile-first safety net: when the links no longer fit on one line they wrap instead of overflowing the viewport.

Grid for the page

The content-plus-sidebar area is two-dimensional on wide screens (two columns, rows determined by content) and one column on narrow screens. Grid handles both with one property change.

.layout {
  display: grid;
  gap: 2rem;
  padding: 1rem;
  max-width: 1100px;
  margin: 0 auto;
}
@media (min-width: 48em) {
  .layout {
    grid-template-columns: minmax(0, 2fr) minmax(16rem, 1fr);
  }
}

With no grid-template-columns, a grid has one column and every child becomes a row: exactly the narrow layout. At 48em (768px at the default font size) the media query adds two columns. The fr unit shares out the free space: 2fr 1fr gives the main column two thirds. The minmax(0, 2fr) on the main column is not decoration: a grid track's default minimum is auto, which is the content's minimum width, so a long unbreakable string such as a URL or a wide code block would force the column wider than its share and push the sidebar off the page. minmax(0, ...) lets the column shrink and the content overflow inside it instead.

Why em in the media query instead of px? If a reader sets their browser's default font size to 20px, 48em becomes 960px and the two-column layout waits until there is really room for the larger text. Pixel breakpoints ignore that.

Cards: Grid without a media query

A grid of cards that fits as many as the width allows is the one place where Grid needs no breakpoint at all:

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

Read it from the inside out. Each column is at least 16rem and at most one fraction of the space; auto-fill creates as many such columns as fit. The min(100%, 16rem) guards against a container narrower than 16rem (small phones with large text), where a plain 16rem minimum would overflow. One line, works at every width.

Flexbox inside each card

Cards in a row should have their footers aligned even when the text lengths differ. Grid has already made every card in a row the same height (grid items stretch by default). Inside the card, Flexbox in column direction pushes the footer down:

.card {
  display: flex;
  flex-direction: column;
  border: 1px solid #ccc;
  padding: 1rem;
}
.card .meta { margin-top: auto; }   /* takes all free space above it */

margin-top: auto on a flex item absorbs the leftover space, so the element sits at the bottom. The same trick with margin-left: auto in a row pushes one item to the right.

Choosing between them

SituationUse
Items in a line whose sizes come from their content (nav links, tag chips, toolbar buttons)Flexbox
Centring one thing in a boxEither: display: grid; place-items: center is shortest
Page regions that must line up in both directions (header, sidebar, main, footer)Grid
Equal-width columns of cards that wrapGrid with auto-fill
Form rows: label beside input, with wrappingFlexbox with flex-wrap
Overlapping elements (a caption on an image)Grid: put both in the same cell with grid-area: 1 / 1

They nest freely. A Grid cell can contain a flex container and vice versa, and that is how most real pages are built.

Pitfalls

  • Fixed heights. Setting height: 100vh on the layout and then adding content gives you overflow. Use min-height and let content decide.
  • Forgetting min-width: 0 on flex items. A flex item will not shrink below its content's minimum width unless you set min-width: 0 (or overflow: hidden). This is the Flexbox version of the Grid minmax(0, ...) problem above, and it is the usual cause of a flex row that overflows on phones.
  • Margins for spacing between items. Use gap. Margins on items leave a gap at the edge and double up when items wrap.
  • Reordering visually only. order and grid-area change where things appear, not the tab order or reading order. Keep the source order sensible.
  • Percentage widths from the float era. width: 33.33% with float: left still works, but it fights with padding and gap. Let Grid compute the columns.

Exercise

Build the page above with the CSS shown, then add a third region: a full-width "featured" strip between the header and the two-column area. On wide screens make the sidebar stick to the top as the page scrolls (position: sticky; top: 1rem; align-self: start on the aside). Resize the window from 320px to 1400px and check that nothing overflows horizontally; the browser's device toolbar in developer tools lets you drag the width.

Summary

  • Flexbox: one axis, content-sized items, justify-content, align-items, gap, flex-wrap.
  • Grid: two axes, grid-template-columns with fr and minmax, repeat(auto-fill, ...) for responsive card grids.
  • Mobile first: default styles are the single-column layout; min-width media queries in em add columns.
  • The two overflow traps: minmax(0, 1fr) for grid tracks and min-width: 0 for flex items.

Next: make the type scale with the viewport using clamp().