Skip to main content
Published / updated

Layout with Flexbox and Grid

Before you start

You need: CSS fundamentals (Article 04).

Time: about 50 minutes, plus the practice.

Learning objective

Choose between Flexbox and Grid for a given layout and build it without floats, hacks or magic numbers.

Topics

  • Display and normal flow
  • Flexbox: axes, alignment, growth
  • Grid: tracks, areas, auto placement
  • Choosing between them
  • gap
  • Positioning
  • Stacking contexts and z-index
  • Overflow

Display and normal flow

display: block; /* full width, stacks vertically */
display: inline; /* flows with text, no width or vertical margin */
display: inline-block; /* flows with text, accepts width and height */
display: flex; /* children become flex items */
display: grid; /* children become grid items */
display: none; /* removed from layout and from screen readers */

visibility: hidden keeps the space and hides the element; display: none removes it entirely. Only display: none hides it from assistive technology — which is what you usually want, and occasionally exactly what you do not.

Flexbox

One-dimensional: items lay out along a single axis, wrapping if allowed.

.toolbar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
PropertyControls
flex-directionrow, row-reverse, column, column-reverse
justify-contentAlignment along the main axis
align-itemsAlignment along the cross axis
align-contentSpacing of wrapped lines
flex-wrapnowrap (default), wrap
gapSpace between items

The axes swap with flex-direction. In a row, main is horizontal and cross is vertical; in a column, the reverse. So justify-content: center centres horizontally in a row and vertically in a column — the source of most Flexbox confusion.

justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly;
align-items: stretch | flex-start | flex-end | center | baseline;

align-items: stretch is the default, which is why flex children are equal height without effort — useful for a row of cards.

Item properties

.item {
flex-grow: 1; /* share of leftover space */
flex-shrink: 1; /* how readily it shrinks */
flex-basis: 200px; /* starting size before growing or shrinking */

flex: 1; /* 1 1 0 — equal widths regardless of content */
flex: auto; /* 1 1 auto — grow from content size */
flex: none; /* 0 0 auto — fixed */
flex: 0 0 200px; /* fixed 200px, no growing or shrinking */
}

flex: 1 versus flex: auto is worth understanding: flex: 1 sets basis to 0, so items end up equal regardless of content; flex: auto starts from content width, so a longer item stays wider.

.item { align-self: flex-end; } /* override align-items for one item */
.item { order: -1; } /* move it visually first */

order changes visual order only. Tab order and screen-reader order follow the DOM. Using it to rearrange interactive elements makes the page confusing to navigate by keyboard.

A common flex layout

.page {
display: flex;
flex-direction: column;
min-height: 100vh;
}

.page main {
flex: 1; /* pushes the footer down on short pages */
}
.card-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}

.card-row > .card {
flex: 1 1 280px; /* at least 280px, grow to fill, wrap when it cannot */
}

That second rule is a responsive card grid with no media queries at all.

Grid

Two-dimensional: rows and columns together.

.layout {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
gap: 1rem;
min-height: 100vh;
}

fr is a fraction of the remaining space — 1fr takes what is left after the fixed 240px.

Named areas

.layout {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
gap: 1rem;
min-height: 100vh;
}

.layout > header { grid-area: header; }
.layout > nav { grid-area: sidebar; }
.layout > main { grid-area: main; }
.layout > footer { grid-area: footer; }

The template is a readable picture of the layout, and rearranging it for mobile is one property:

@media (max-width: 768px) {
.layout {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
}

Note the sidebar moved below main — with areas, that is a template change, not a DOM change.

Placing items explicitly

.wide-card {
grid-column: 1 / 3; /* from line 1 to line 3 */
grid-column: span 2; /* span two tracks */
grid-column: 1 / -1; /* full width, whatever the column count */
grid-row: 2 / 4;
}

1 / -1 is the useful one: full width regardless of how many columns the grid has.

Responsive without media queries

.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}

As many columns as fit at 280px minimum, each sharing the space equally. One line, fully responsive, no breakpoints.

auto-fit collapses empty tracks so a single item fills the row; auto-fill keeps them, leaving gaps. auto-fit is usually what you want.

Implicit tracks

.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: minmax(120px, auto);
gap: 1rem;
}

Rows beyond the template are created automatically. grid-auto-rows sizes them — minmax(120px, auto) gives a minimum height that grows with content.

Choosing between them

Use Flexbox forUse Grid for
One dimension — a row or a columnTwo dimensions together
A toolbar, a nav bar, a button groupPage layout
Content-driven sizingLayout-driven sizing
Centring one thingA card grid
Distributing space along a lineAligned rows and columns

They nest freely, and most real pages use both: Grid for the page shell, Flexbox inside each component.

.page { display: grid; grid-template-areas: /* ... */; }
.toolbar { display: flex; justify-content: space-between; align-items: center; }
.card { display: flex; flex-direction: column; }

Centring, for reference:

/* Flexbox */
.centre { display: flex; justify-content: center; align-items: center; }

/* Grid — shorter */
.centre { display: grid; place-items: center; }

gap

.row { display: flex; gap: 1rem; }
.grid { display: grid; gap: 1.5rem 1rem; } /* row-gap column-gap */

gap replaces the old margin-on-every-child-then-remove-the-last pattern:

/* The old way */
.card { margin-right: 1rem; }
.card:last-child { margin-right: 0; }

/* Now */
.row { gap: 1rem; }

Space appears between items only, never on the outer edges, and it works in both Flexbox and Grid.

Positioning

position: static; /* default — normal flow */
position: relative; /* offset from its normal position, space retained */
position: absolute; /* removed from flow, positioned to nearest positioned ancestor */
position: fixed; /* positioned to the viewport */
position: sticky; /* relative until it hits a threshold, then fixed */
.badge-holder { position: relative; }

.badge {
position: absolute;
top: -8px;
right: -8px;
}

absolute positions relative to the nearest ancestor whose position is not static. Forgetting position: relative on the parent is why a badge ends up in the page's top-right corner instead of the card's.

thead th {
position: sticky;
top: 0;
background: #fff;
z-index: 1;
}

Sticky table headers, with no JavaScript. Two requirements: an explicit top (or other offset), and an ancestor that does not have overflow: hidden — which silently disables sticky and is genuinely hard to find.

The sticky element also cannot escape its parent, so it stops sticking once the parent scrolls away.

Stacking and z-index

z-index only applies to positioned elements (and flex or grid items).

.modal-backdrop { position: fixed; z-index: 100; }
.modal { position: fixed; z-index: 101; }

Stacking contexts

A new stacking context is created by: a positioned element with a z-index other than auto, opacity below 1, transform, filter, will-change, or isolation: isolate.

Inside a stacking context, z-index is scoped to that context. A child with z-index: 9999 cannot escape a parent whose context sits below another:

.card { position: relative; z-index: 1; }
.tooltip { position: absolute; z-index: 9999; } /* still trapped inside .card */
.header { position: relative; z-index: 2; } /* covers the tooltip */

This is the cause of nearly every "z-index does not work". The tooltip's 9999 is compared only against its siblings inside .card; .card itself is what competes with .header.

Two fixes: raise the ancestor's z-index, or move the tooltip out of that subtree — which is what portals in React and Angular exist for.

Do not use arbitrary large numbers. Define a scale:

:root {
--z-dropdown: 10;
--z-sticky: 20;
--z-modal: 30;
--z-toast: 40;
}

Note that opacity: 0.99 creates a stacking context. A fade animation can therefore change stacking mid-transition, which produces a flicker nobody can explain.

Overflow

overflow: visible; /* default — content spills out */
overflow: hidden; /* clipped */
overflow: scroll; /* always scrollable */
overflow: auto; /* scrollable only when needed */
overflow-x: auto;
overflow-y: hidden;
.table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}

A wide table scrolls inside its container while the page does not. The page body must never scroll horizontally — it is the most common mobile layout defect, and its usual causes are a fixed width, a long unbroken string, or an image without max-width: 100%.

Find it by binary search in DevTools: set * { outline: 1px solid red } and look for the element extending past the viewport.

img { max-width: 100%; height: auto; }

.wrap-anywhere {
overflow-wrap: break-word;
word-break: break-word;
}

Long unbroken text — an email address, a URL, a roll number — overflows its container without overflow-wrap.

overflow: hidden breaks position: sticky on any descendant. When sticky stops working, walk up the ancestors looking for it.

A complete layout

<div class="layout">
<header class="site-header">
<h1>NexCoding Academy</h1>
<nav aria-label="Main">...</nav>
</header>

<nav class="sidebar" aria-label="Sections">...</nav>

<main>
<div class="toolbar">
<h2>Students</h2>
<button type="button">Add student</button>
</div>

<div class="card-grid">
<article class="card">...</article>
<article class="card">...</article>
</div>
</main>

<footer class="site-footer">...</footer>
</div>
.layout {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
min-height: 100vh;
gap: 1rem;
}

.site-header { grid-area: header; }
.sidebar { grid-area: sidebar; }
main { grid-area: main; min-width: 0; }
.site-footer { grid-area: footer; }

.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}

.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}

.card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1.5rem;
border: 1px solid var(--colour-border);
border-radius: var(--radius);
}

.card .actions { margin-top: auto; } /* pin actions to the bottom */

@media (max-width: 768px) {
.layout {
grid-template-columns: 1fr;
grid-template-areas: "header" "main" "sidebar" "footer";
}
}

min-width: 0 on the grid item is the non-obvious line. Grid and flex items default to min-width: auto, which refuses to shrink below their content — so one wide table inside main makes the whole page scroll sideways. min-width: 0 allows shrinking, and the table scrolls inside its own container instead.

margin-top: auto on the last flex child pushes it to the bottom, so cards of different heights have their buttons aligned.

Errors you will hit

What you seeCauseFix
Flex children overflow the containerNo min-width: 0 on a flex itemAdd it
justify-content does nothingWrong axis — it follows flex-directionCheck the direction
Grid items land in the wrong cellsImplicit rows createdDefine grid-template-rows, or check grid-auto-flow
A table overflows on mobileTables do not shrinkWrap it in overflow-x: auto
height: 100% does nothingThe parent has no heightSet a height, or use min-height: 100vh

justify-content and align-items swap meaning when flex-direction is column. That single fact explains most "flexbox is ignoring me" moments.

Common mistakes

  • Floats for layout
  • Confusing main and cross axes after changing flex-direction
  • order used to rearrange interactive elements, breaking tab order
  • position: absolute without a positioned ancestor
  • overflow: hidden on an ancestor, silently breaking position: sticky
  • position: sticky with no offset
  • Escalating z-index values instead of understanding stacking contexts
  • A transform or opacity creating an unexpected stacking context
  • Missing min-width: 0, so one wide child forces horizontal page scroll
  • max-width: 100% missing on images
  • Margin hacks where gap would do
  • Media queries where auto-fit and minmax would suffice

Practice

The course exercise is recreate a responsive card layout.

  1. Build the full page layout above with Grid areas. Confirm the footer sits at the bottom on a short page.
  2. Build the card grid with auto-fit and minmax. Resize the window and confirm columns change with no media query.
  3. Build the toolbar with Flexbox and space-between. Add flex-wrap and confirm it stacks on narrow screens.
  4. Give the cards different content lengths and use margin-top: auto to align the buttons.
  5. Change flex-direction to column on the toolbar and observe what justify-content: center now does.
  6. Compare flex: 1 and flex: auto on three items with different content lengths.
  7. Put a badge on a card with position: absolute. Remove position: relative from the card and record where it goes.
  8. Make a table header sticky. Then add overflow: hidden to an ancestor and confirm it stops working.
  9. Create the trapped-tooltip situation: .card { position: relative; z-index: 1 }, a tooltip at z-index: 9999, and a header at z-index: 2. Confirm the header covers the tooltip, then fix it two ways.
  10. Put a very wide table inside the grid's main area with no min-width: 0. Confirm the whole page scrolls sideways, then add it.
  11. Add a long unbroken email address to a card and fix the overflow with overflow-wrap.
  12. Rearrange the layout for mobile using only grid-template-areas.

Then run the course debugging exercise — fix layout overflow. Deliberately create horizontal page scroll three ways: a fixed width wider than the viewport, an image with no max-width, and a missing min-width: 0. Find each with the * { outline: 1px solid red } technique.

Exercises 9 and 10 are the two that consume the most time in real projects.

You can now

  • Choose between Flexbox and Grid for a given problem
  • Build a responsive card layout
  • Stop a wide table breaking the page
  • Say why height: 100% often does nothing
  • Debug a layout in the DevTools grid and flex overlays

Review questions

  1. When would you use Grid rather than Flexbox?
  2. Why does justify-content: center behave differently after changing flex-direction?
  3. Why can a child with z-index: 9999 still be covered by an element with z-index: 2?
  4. What does min-width: 0 on a grid item fix?

Next: Responsive design