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;
}
| Property | Controls |
|---|---|
flex-direction | row, row-reverse, column, column-reverse |
justify-content | Alignment along the main axis |
align-items | Alignment along the cross axis |
align-content | Spacing of wrapped lines |
flex-wrap | nowrap (default), wrap |
gap | Space 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 for | Use Grid for |
|---|---|
| One dimension — a row or a column | Two dimensions together |
| A toolbar, a nav bar, a button group | Page layout |
| Content-driven sizing | Layout-driven sizing |
| Centring one thing | A card grid |
| Distributing space along a line | Aligned 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 see | Cause | Fix |
|---|---|---|
| Flex children overflow the container | No min-width: 0 on a flex item | Add it |
justify-content does nothing | Wrong axis — it follows flex-direction | Check the direction |
| Grid items land in the wrong cells | Implicit rows created | Define grid-template-rows, or check grid-auto-flow |
| A table overflows on mobile | Tables do not shrink | Wrap it in overflow-x: auto |
height: 100% does nothing | The parent has no height | Set 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 orderused to rearrange interactive elements, breaking tab orderposition: absolutewithout a positioned ancestoroverflow: hiddenon an ancestor, silently breakingposition: stickyposition: stickywith no offset- Escalating
z-indexvalues instead of understanding stacking contexts - A
transformoropacitycreating 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
gapwould do - Media queries where
auto-fitandminmaxwould suffice
Practice
The course exercise is recreate a responsive card layout.
- Build the full page layout above with Grid areas. Confirm the footer sits at the bottom on a short page.
- Build the card grid with
auto-fitandminmax. Resize the window and confirm columns change with no media query. - Build the toolbar with Flexbox and
space-between. Addflex-wrapand confirm it stacks on narrow screens. - Give the cards different content lengths and use
margin-top: autoto align the buttons. - Change
flex-directiontocolumnon the toolbar and observe whatjustify-content: centernow does. - Compare
flex: 1andflex: autoon three items with different content lengths. - Put a badge on a card with
position: absolute. Removeposition: relativefrom the card and record where it goes. - Make a table header sticky. Then add
overflow: hiddento an ancestor and confirm it stops working. - Create the trapped-tooltip situation:
.card { position: relative; z-index: 1 }, a tooltip atz-index: 9999, and a header atz-index: 2. Confirm the header covers the tooltip, then fix it two ways. - Put a very wide table inside the grid's
mainarea with nomin-width: 0. Confirm the whole page scrolls sideways, then add it. - Add a long unbroken email address to a card and fix the overflow with
overflow-wrap. - 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
- When would you use Grid rather than Flexbox?
- Why does
justify-content: centerbehave differently after changingflex-direction? - Why can a child with
z-index: 9999still be covered by an element withz-index: 2? - What does
min-width: 0on a grid item fix?
Next: Responsive design