Chapter 4
Flexbox and CSS Grid
One- and two-dimensional layout systems with real layout recipes.
4. Flexbox and CSS Grid
Flexbox and Grid are the browser's main tools for arranging content in two dimensions. They are not competing replacements for one another: Flexbox distributes items along one axis, while Grid defines rows and columns together. Choosing the model that matches the relationships in the design makes layouts simpler to adapt and easier to understand.
This chapter develops a practical model for both systems, including how Flexbox distributes space, how Grid tracks are sized, and how to combine them into responsive page layouts.
Overview & Objectives
By the end of this chapter, you will be able to:
- Choose Flexbox for a one-dimensional relationship and Grid for a two-dimensional one.
- Reason about Flexbox's main axis, cross axis, sizing, wrapping, and alignment.
- Define Grid tracks with fixed, flexible, and intrinsic sizing functions.
- Use
fr,minmax(), andrepeat(auto-fit, minmax(...))for adaptive grids. - Build named layouts with
grid-template-areasand coordinate nested tracks withsubgrid. - Implement a dashboard, a holy-grail page, and a responsive card gallery.
- Diagnose common sizing and alignment problems without using arbitrary margins.
One-Dimensional and Two-Dimensional Layout
Flexbox lays out items along one primary axis at a time. A row of buttons can distribute horizontally and wrap onto later lines; a column can stack a sidebar's contents vertically. Flexbox can wrap into multiple lines, but each line is primarily sized and aligned independently.
Grid defines rows and columns together. A dashboard can align cards into shared columns and rows, and a page can allocate a header, sidebar, content region, and footer to named areas.
Use Flexbox when the layout question is “How should these items be arranged along this line?” Use Grid when it is “How should these elements relate across rows and columns?” This is a guideline, not a restriction. A grid cell can contain a flex row, and a grid itself can be a flex item. Real interfaces often combine both.
Start from the content and its relationships, not from the tool. A horizontal group whose items have different natural widths is often a good flex row. A gallery whose cards should form consistent columns is often a good grid. If the layout becomes a tangle of offsets and forced widths, step back and select the model that directly describes the structure.
Flexbox: Axes, Sizing, and Alignment
To make an element a flex container, set display: flex or display: inline-flex. Its direct children become flex items:
.toolbar {
display: flex;
}By default, flex-direction: row places items along the main axis in the inline direction. In a typical left-to-right writing mode, that appears horizontal. The cross axis is perpendicular. With flex-direction: column, the main axis follows the block direction and the cross axis follows the inline direction.
Thinking in main and cross axes is more reliable than memorizing “horizontal” and “vertical,” especially when writing modes or flex-direction change.
Direction, wrapping, and order
.toolbar {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.75rem;
}flex-directionsets the main axis:row,row-reverse,column, orcolumn-reverse.flex-wrap: wrapallows items to form multiple flex lines when they cannot fit on one line. The default isnowrap.gapcreates consistent spacing between items and flex lines without adding edge margins.
Reverse directions change visual placement, but they do not reorder the document. Keyboard and assistive-technology reading order generally follows the DOM. Avoid using order or reverse directions to create an order that contradicts the source order; use them for minor visual adjustments only when the reading and focus order remains sensible.
The flex sizing model
The shorthand flex combines three properties:
.item {
flex: 1 1 12rem;
}This is flex-grow: 1, flex-shrink: 1, and flex-basis: 12rem.
flex-basisis the initial main-size suggestion for an item before free space is distributed. A length such as12remestablishes a preferred starting size.autouses the item's main-size property when specified, otherwise its content size.flex-growis a non-negative factor for distributing positive free space. Items with larger grow factors receive a larger share relative to their peers.flex-shrinkis a non-negative factor for distributing negative free space when items would otherwise overflow. Shrinking is weighted by the shrink factor and the item's base size, so a large item may surrender more space than a small one.
Consider three items with equal bases and flex-grow: 1: available positive space is divided equally. If one has a larger grow factor, it receives a correspondingly larger share of the growable space. With negative free space, flex-shrink: 1 allows items to shrink, but the algorithm respects minimum-size constraints; it does not guarantee that every item will become arbitrarily small.
Common shorthand patterns:
.equal-columns { flex: 1; }
.natural-size { flex: 0 0 auto; }
.sidebar { flex: 0 0 16rem; }flex: 1 is commonly expanded by browsers as 1 1 0%, not literally 1 1 0. In a definite-size container, this usually makes items share available space from a zero basis. The percentage basis can behave differently when the container's main size is indefinite. If that distinction matters, set the three longhands or use an explicit basis such as 0.
Why a flex item refuses to shrink
Flex items have an automatic minimum size. In many common row layouts, this means a long unbreakable word, a wide image, or a long code snippet can keep an item wider than expected. The item appears to ignore flex-shrink because it has reached its minimum-content size.
Allow the item to shrink when appropriate:
.main-content {
min-width: 0;
}
.main-content pre {
overflow-x: auto;
}For a column flex container, the relevant constraint is often min-height: 0. Apply the minimum-size override to the element that needs to shrink, rather than adding it everywhere without checking. Also give oversized media appropriate constraints, such as max-width: 100%.
Alignment
Flex alignment properties act on specific axes:
| Property | Axis or effect |
|---|---|
justify-content |
Distributes items along the main axis when free space is available. |
align-items |
Aligns items on the cross axis within a line. |
align-self |
Overrides cross-axis alignment for one item. |
align-content |
Distributes multiple flex lines across the cross axis when the container has extra space. |
gap |
Adds spacing between items and between wrapped lines. |
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}align-content does not move a single flex line into the center; it distributes space between multiple lines. Use align-items for the items in one line. In a column flex container, the main axis is vertical, so justify-content works vertically and align-items works horizontally.
For a single item that should push to the end of a row, margin-inline-start: auto is often clearer than changing how the entire row distributes space. For a group with known gaps, gap is usually more predictable than justify-content: space-between, which distributes leftover space and can produce unexpectedly wide gaps.
Grid: Tracks, Lines, and Placement
A grid container establishes a two-dimensional grid for its direct children:
.layout {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr);
grid-template-rows: auto 1fr auto;
gap: 1rem;
}The declared columns and rows form the explicit grid. Each track is the space between two grid lines. A grid item can be placed using line numbers, named lines, spans, or named areas. Items placed outside the explicit grid can create implicit tracks; grid-auto-rows and grid-auto-columns control their sizing.
Grid's auto-placement algorithm places items in source order when explicit positions are omitted. It can fill rows or columns, and grid-auto-flow: dense can backfill earlier holes with later items. Dense packing can make visual order differ from DOM and keyboard order, so avoid it when it could confuse a meaningful sequence.
fr: a share of flexible space
The fr unit represents a share of the grid container's available free space after non-flexible track sizes and gaps have been accounted for. It is not simply a percentage:
.split {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 1rem;
}When tracks can use the remaining space, the second track receives twice the flexible share of the first. Content minimums and track sizing rules still matter; 1fr does not always mean “shrink to any size necessary.”
For a content column that must be allowed to shrink below its min-content contribution, use minmax(0, 1fr):
.article-layout {
display: grid;
grid-template-columns: 15rem minmax(0, 1fr);
gap: 2rem;
}The 0 minimum allows that track to shrink to the space available. Without it, an automatic minimum can let long content expand the grid beyond its container. This is a common source of horizontal overflow.
minmax() and intrinsic sizing
minmax(minimum, maximum) defines a track's permitted size range. Each side can be a fixed length, a flexible value, or an intrinsic sizing keyword where allowed:
.results {
display: grid;
grid-template-columns: minmax(14rem, 1fr) 2fr;
}The first track should not be smaller than 14rem, but can grow to take a flexible share. The min-content keyword gives the smallest size that avoids soft wrapping; max-content describes the size needed to fit content without soft wrapping. Intrinsic sizes are useful, but long unbreakable values can make them larger than expected.
minmax(0, 1fr) and minmax(14rem, 1fr) express different contracts: the first permits shrinking to zero; the second enforces a minimum that may cause overflow on a narrow viewport. Choose the minimum based on the design's behavior, and add a breakpoint or another layout mode if the minimum no longer fits.
Responsive card galleries with auto-fit
CSS Grid can choose a number of columns from the available width, without a breakpoint for every column count:
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1rem;
}The browser repeats as many tracks as fit. Each track should be at least a useful card width, up to the available flexible space. min(100%, 16rem) prevents the minimum from exceeding a very narrow container.
auto-fitcollapses empty repeated tracks after placement, allowing occupied tracks to stretch into their space.auto-fillretains the empty repeated tracks, preserving the potential column positions.
With a full row of cards, auto-fit and auto-fill often look the same. Their difference is most visible when there are fewer cards than could fit in a row. Use auto-fit when the existing cards should expand; use auto-fill when preserving empty column slots is useful.
Naming and Nesting Grid Layouts
grid-template-areas
Named areas make page-level relationships visible in CSS:
.page {
display: grid;
grid-template-columns: 15rem minmax(0, 1fr);
grid-template-areas:
"header header"
"sidebar content"
"footer footer";
gap: 1rem;
}
.page-header { grid-area: header; }
.page-sidebar { grid-area: sidebar; }
.page-content { grid-area: content; min-width: 0; }
.page-footer { grid-area: footer; }Each quoted row must have the same number of area cells. Repeating the same name in adjacent cells makes a rectangular region. An area cannot form an arbitrary L shape. A period (.) marks an empty cell.
Named areas are excellent for a small number of major regions and responsive rearrangements. For intricate data-driven placement, line names or placement properties can be easier to compose. Keep the DOM in a meaningful reading order; visual placement should not create a surprising keyboard or screen-reader order.
subgrid
By default, a nested grid establishes its own tracks. subgrid lets a nested grid adopt tracks from its parent along one or both axes:
.catalog {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}
.catalog-card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}In a suitable structure, cards can align their image, title, and action rows across neighboring cards even when the titles wrap onto different numbers of lines. A subgrid must participate in the parent grid tracks it is meant to reuse; the row span above reserves the rows the card's nested grid will subgrid. It does not create independent, arbitrarily sized child tracks.
subgrid is supported in current evergreen browsers, but always consider your audience and target browser policy. For browsers where it is unavailable, define a useful fallback layout before the subgrid declaration or accept that cards may align only within themselves. Do not add redundant wrappers solely to force alignment if a simpler layout meets the design.
When to Choose Flexbox or Grid
| Layout need | Starting point | Reason |
|---|---|---|
| Toolbar whose items size from their content | Flexbox | One main axis with natural item sizing. |
| Navigation that wraps when space is limited | Flexbox with flex-wrap |
Items flow along a line and can move to another line. |
| Dashboard cards aligned to common rows and columns | Grid | Both axes and shared tracks matter. |
| A page skeleton with named regions | Grid | Relationships among multiple page regions are explicit. |
| A card's footer pushed to its bottom | Flexbox column inside the card | A one-dimensional distribution within one component. |
| A gallery with as many columns as fit | Grid with auto-fit |
Tracks respond to container space and share available width. |
| One element pushed to the end of a row | Flexbox with an auto margin | A single-axis relationship with a clear spacer. |
It is fine to use Grid for a single axis when its track sizing makes the problem clearer. It is also fine to use Flexbox in a page-level layout when the content truly behaves as one line. The best tool is the one that describes the relationship directly and adapts without fragile offsets.
Responsive Layout Recipes
The following examples are complete layout rules that can be applied to semantic HTML. In each, the source order should match a sensible reading order.
1. Dashboard with aligned cards
<main class="dashboard">
<header class="dashboard__header">...</header>
<section class="dashboard__stats" aria-label="Key statistics">
<article class="stat-card">...</article>
<article class="stat-card">...</article>
<article class="stat-card">...</article>
</section>
<section class="dashboard__activity" aria-labelledby="activity-title">
<h2 id="activity-title">Recent activity</h2>
...
</section>
<aside class="dashboard__aside" aria-label="Quick actions">...</aside>
</main>.dashboard {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 1rem;
}
.dashboard__header {
grid-column: 1 / -1;
}
.dashboard__stats {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
gap: 1rem;
}
.dashboard__activity {
grid-column: 1 / span 8;
min-width: 0;
}
.dashboard__aside {
grid-column: 9 / -1;
}
@media (max-width: 48rem) {
.dashboard__activity,
.dashboard__aside {
grid-column: 1 / -1;
}
}The outer 12-column grid handles the dashboard's major regions. The nested stats grid independently packs its cards. minmax(0, 1fr) allows outer tracks to shrink, and min-width: 0 helps prevent a wide activity table from forcing its grid item past its assigned width. At narrower widths, the activity and aside occupy the full row.
If an activity table still needs more width than its panel, give the table a deliberate horizontal scrolling wrapper; do not make the whole dashboard wider than the viewport.
2. Holy-grail page
<div class="site-layout">
<header class="site-header">...</header>
<nav class="site-nav" aria-label="Primary">...</nav>
<main class="site-main">...</main>
<aside class="site-aside">...</aside>
<footer class="site-footer">...</footer>
</div>.site-layout {
min-height: 100vh;
min-height: 100dvh;
display: grid;
grid-template-columns: 14rem minmax(0, 1fr) 16rem;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
gap: 1rem;
}
.site-header { grid-area: header; }
.site-nav { grid-area: nav; }
.site-main { grid-area: main; min-width: 0; }
.site-aside { grid-area: aside; }
.site-footer { grid-area: footer; }
@media (max-width: 56rem) {
.site-layout {
grid-template-columns: 12rem minmax(0, 1fr);
grid-template-rows: auto auto 1fr auto auto;
grid-template-areas:
"header header"
"nav nav"
"main main"
"aside aside"
"footer footer";
}
}The middle row expands to fill available vertical space because the grid has a definite minimum height and uses 1fr. At the narrower layout, navigation becomes a full-width row, while the main content and complementary sidebar stack. The DOM order remains header, navigation, main, aside, footer in both layouts, so visual rearrangement agrees with reading order.
Production implementations should choose breakpoints based on when the content stops fitting, not a device label. If the header's navigation needs a compact interaction on small screens, implement that interaction accessibly rather than shrinking links until they collide.
3. Responsive card gallery
<section class="course-gallery" aria-labelledby="courses-title">
<h2 id="courses-title">Courses</h2>
<div class="course-gallery__grid">
<article class="course-card">...</article>
<article class="course-card">...</article>
<article class="course-card">...</article>
</div>
</section>.course-gallery__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
gap: clamp(1rem, 2vw, 1.5rem);
}
.course-card {
min-width: 0;
display: flex;
flex-direction: column;
padding: 1.25rem;
border: 1px solid #d9deea;
border-radius: 0.75rem;
}
.course-card__description {
overflow-wrap: anywhere;
}
.course-card__action {
margin-block-start: auto;
align-self: flex-start;
}The gallery chooses its own column count based on available space. Flexbox inside each card places its contents along one axis; the auto margin on the action consumes available space and aligns card actions toward the bottom when cards in a row have different content heights. In Grid, items in a shared row generally stretch to the row's size by default, making that internal flex layout useful.
Avoid fixed card heights just to align buttons; translated text, zoom, or a longer description can be clipped. Let the cards grow with their content.
Common Alignment and Sizing Pitfalls
Centering on the wrong axis
justify-content follows the main axis, and align-items follows the cross axis. If the container changes from row to column, the physical direction of those axes changes too. Inspect flex-direction or the grid's row/column axes before changing alignment.
In Grid, justify-items and align-items align items inside their grid areas. justify-content and align-content distribute the grid's tracks within the grid container when there is spare space. These property pairs solve different problems.
align-content appears to do nothing
align-content distributes multiple flex lines or grid tracks across available cross-axis/block space. It has no visible effect when there is no extra space, or when there is only one flex line. For item alignment within a line or cell, use align-items or justify-items.
space-between creates huge gaps
justify-content: space-between distributes free space between items. With only two items or a wide container, the gap can become enormous. If you want a consistent separation, use gap. If one action should move to the far edge, use an auto margin on that item.
Flex items overflow despite flex-shrink
Check automatic minimum sizing, intrinsic content, and unbreakable values. Apply min-width: 0 or min-height: 0 to the relevant flex item, then handle its overflowing content intentionally.
Grid columns overflow
Check whether a 1fr track has an automatic minimum that is wider than the container. Consider minmax(0, 1fr), min-width: 0 on the grid item, and sensible wrapping or overflow for long content. Do not blindly apply overflow: hidden, which can cut off focus indicators and content.
Grid area strings do not line up
Each row in grid-template-areas must have the same number of cells, and each named area must form one rectangle. Use a period for a blank cell. Inspect whitespace and row counts when a declaration is invalid.
Visual order disagrees with keyboard order
Flex order, reverse directions, and dense Grid placement can make items look reordered without changing DOM order. Test focus movement and screen-reader reading order. Prefer changing the markup or use visual changes that preserve a logical sequence.
Debugging workflow
- Select the container in DevTools and inspect
display,flex-direction,flex-wrap, or the computed grid tracks. - Enable Flexbox/Grid overlays to see axes, gaps, track sizes, and item placement.
- Inspect the item that overflows: check its minimum size, intrinsic content,
flex-basis, and whether it has a fixed width. - Temporarily outline items and set a visible
gapto distinguish spacing from margin behavior. - Remove declarations one at a time to identify which sizing constraint creates the overflow.
- Test the narrowest expected width, browser zoom, long labels, and translated content.
- Verify the DOM order and keyboard focus order, not only the screenshot.
Accessibility & Best Practices
- Keep source order meaningful. CSS placement does not change how assistive technology reads the DOM.
- Use Grid for shared two-axis relationships and Flexbox for a primary one-axis distribution; combine them at component boundaries.
- Prefer
gapfor predictable spacing between layout items. - Let content determine row height. Avoid fixed heights on text-bearing cards and controls.
- Use
minmax(0, 1fr)andmin-width: 0when intrinsic minimum sizing should not widen a track. - Use
auto-fitwhen occupied gallery tracks should expand; useauto-fillwhen empty track positions should remain. - Use
grid-template-areasfor clear page-level placement, keeping each area rectangular. - Use
subgridwhen nested content must align to parent tracks, with a fallback consistent with the supported browser policy. - Do not use dense packing, visual reordering, or reverse directions when they make reading and focus order confusing.
- Test layouts at different widths, zoom levels, font sizes, and with long or localized content.
Practice Exercises
Exercise 1 — Choose and implement the layout
A dashboard has a title row, four summary cards that should form as many columns as fit, and a recent-activity panel beside a quick-actions panel on wide screens. On narrow screens, the two panels should stack in reading order. The activity content may include a wide table.
Write the layout CSS using Grid for the dashboard regions and the summary-card collection. Include a reasonable narrow-screen breakpoint and prevent the activity content from forcing the page wider than the viewport. Explain why this uses two nested grids instead of one flex row.
Exercise 2 — Diagnose the toolbar and article overflow
This layout has two bugs: the article pushes beyond the right edge of its panel, and the toolbar actions spread too far apart on wide screens.
.shell {
display: grid;
grid-template-columns: 14rem 1fr;
gap: 1rem;
}
.toolbar {
display: flex;
justify-content: space-between;
}
.article pre {
white-space: pre;
}Write a minimal CSS correction that allows the article track and its code block to remain within the available page width while allowing the code itself to scroll horizontally. Set a consistent toolbar gap and push only the final action to the end. Explain which automatic minimum-size behavior caused the original overflow.
Solutions
Solution 1
.dashboard {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 1rem;
}
.dashboard__title {
grid-column: 1 / -1;
}
.dashboard__summary {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
gap: 1rem;
}
.dashboard__activity {
grid-column: 1 / span 8;
min-width: 0;
}
.dashboard__activity-content {
overflow-x: auto;
}
.dashboard__actions {
grid-column: 9 / -1;
}
@media (max-width: 48rem) {
.dashboard__activity,
.dashboard__actions {
grid-column: 1 / -1;
}
}The outer grid controls the two-dimensional relationship between the title and lower panels. The nested grid handles the card collection, whose column count changes with its available width. At the breakpoint, both panels span the full row and appear in their DOM order. minmax(0, 1fr) and min-width: 0 allow the activity track and item to shrink; the table wrapper scrolls its own wide content rather than widening the page.
A single flex row is less direct here: it can distribute panels on one axis, but it does not describe the shared dashboard columns or summary-card tracks as clearly.
Solution 2
.shell {
display: grid;
grid-template-columns: 14rem minmax(0, 1fr);
gap: 1rem;
}
.article {
min-width: 0;
}
.article pre {
max-width: 100%;
overflow-x: auto;
white-space: pre;
}
.toolbar {
display: flex;
align-items: center;
gap: 0.75rem;
}
.toolbar__last-action {
margin-inline-start: auto;
}The default automatic minimum for a flexible Grid track can be based on its min-content contribution. A long unbreakable code line can therefore make the 1fr track wider than the available space. minmax(0, 1fr) permits the track to shrink; min-width: 0 allows the article grid item to use that smaller width. The code block owns its horizontal overflow, preserving the line while keeping the overall page within its panel.
The toolbar uses a fixed gap for consistent separation. The auto inline-start margin consumes remaining main-axis space before only the last action, instead of distributing all free space between every pair of toolbar items.