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