Open the IRCTC seat-availability screen for a Rajdhani coach. Two structures sit on that page and they are not the same kind of structure. The search bar at the top — origin station, destination, date, class, a "Search" button — is a single line of controls that needs to squeeze, grow, and realign as the viewport changes width. The seat map below it — rows of berths, columns for window/middle/side-lower/side-upper — is a genuine two-dimensional table where a seat's identity depends on both its row and its column at once. For fifteen years, CSS had no layout model that matched either structure honestly. Developers built both with floats, negative margins, and `display: inline-block` hacks that broke the moment content length changed. Flexbox (stable across major browsers by 2015) and CSS Grid (shipped in Chrome, Firefox, and Safari in March 2017) finally gave the search bar and the seat map their own native models: Flexbox for the one-dimensional line, Grid for the two-dimensional table. This chapter builds both from first principles, derives exact pixel outputs by hand, and corrects the single mistake almost every self-taught developer makes about `justify-content`.
Why normal flow was never enough
By default, every HTML element lays out in normal flow: block-level elements (`div`, `p`, `section`) stack vertically, one full-width box per line; inline elements (`span`, `a`) flow horizontally like words in a sentence, wrapping when they run out of room. Normal flow answers exactly one layout question — "what comes next in the document" — and answers it in one fixed way. It cannot say "these three cards should share the remaining horizontal space in a 1:2:1 ratio" or "this sidebar occupies column 1, this header occupies columns 2 through 4." Those are not formatting details; they require an actual algorithm to solve, because the browser must resolve competing space claims from multiple boxes simultaneously. Flexbox and Grid are exactly that: two different constraint-solving algorithms, each exposed to you through a fixed set of CSS properties. Understanding them means understanding the algorithm, not memorizing property names.
Flexbox: solving one axis
Flexbox (the CSS Flexible Box Layout Module) solves layout along a single axis at a time. Setting `display: flex` on a container does two things: it establishes a main axis (the direction items are laid out in, controlled by `flex-direction`, default `row` — left to right) and a cross axis, perpendicular to it. Every alignment property in Flexbox is defined relative to these two axes, not to "horizontal" and "vertical" in any absolute sense — a distinction that causes the single most common Flexbox bug, corrected below.
Container-level properties: `flex-direction` (row / row-reverse / column / column-reverse — which axis is "main"), `flex-wrap` (whether items that overflow the main axis wrap onto new lines), `justify-content` (how items are distributed along the main axis — flex-start, center, space-between, space-around, space-evenly), `align-items` (how items are aligned along the cross axis — stretch, flex-start, center, baseline), and `gap` (fixed space between items, unaffected by grow/shrink math).
Item-level properties: `flex-grow` (a unitless ratio describing how eagerly this item claims positive free space relative to its siblings), `flex-shrink` (the same, for negative free space when items overflow the container), `flex-basis` (the item's starting size along the main axis before grow/shrink is applied — this is what the browser measures free space against, not `width`), and the `flex` shorthand that sets all three at once. `align-self` overrides `align-items` for one item; `order` changes visual order without touching the DOM.
Worked example 1: exact pixel widths from flex-grow
Build the toolbar for a school cricket-tournament dashboard — three stat cards inside a 960px-wide flex container with an 8px gap... actually 10px gap between cards:
<div class="stat-bar">
<div class="matches">Matches Played: 14</div>
<div class="winrate">Win Rate: 71%</div>
<div class="nrr">Net Run Rate: +0.82</div>
</div>
.stat-bar {
display: flex;
width: 960px;
gap: 10px;
}
.matches { flex: 1 200px; } /* grow:1 shrink:1 (default) basis:200px */
.winrate { flex: 2 150px; } /* grow:2 shrink:1 (default) basis:150px */
.nrr { flex: 1 100px; } /* grow:1 shrink:1 (default) basis:100px */
The browser's algorithm (CSS Flexbox spec §9.7) runs in three steps. Step 1: sum the flex-basis values — 200 + 150 + 100 = 450px. Step 2: compute free space — container width minus basis sum minus gaps: 960 − 450 − (2 × 10) = 490px, all of it positive, so this is a growing case. Step 3: sum the grow factors — 1 + 2 + 1 = 4 — and divide the free space by that sum to get one "grow unit": 490 ÷ 4 = 122.5px. Each item then receives basis + (its own grow factor × grow unit):
| Card | flex-basis | flex-grow | Share of 490px free space | Final width |
|---|---|---|---|---|
| Matches | 200px | 1 | 1 × 122.5 = 122.5px | 322.5px |
| Win Rate | 150px | 2 | 2 × 122.5 = 245.0px | 395.0px |
| Net Run Rate | 100px | 1 | 1 × 122.5 = 122.5px | 222.5px |
Check: 322.5 + 395.0 + 222.5 = 940px, plus the two 10px gaps = 960px — exactly the container width, confirming the derivation. Win Rate ends up about 22% wider than Matches Played (395px vs 322.5px) not because of its basis (150 < 200) but purely because its grow factor is double — this is the entire point of `flex-grow`: it is a ratio of additional space, applied on top of whatever basis each item starts with.
A precision point most tutorials skip
The mirror-image case — items shrinking because their combined basis exceeds the container — does not use `flex-shrink` alone. The browser computes a scaled shrink factor for each item as flex-shrink × flex-basis, then distributes the negative free space in proportion to those scaled factors, not to the raw shrink values. Two items with identical `flex-shrink: 1` but different basis values (say 600px and 100px) will not lose equal pixels when the container is too narrow — the 600px item, being physically larger, is weighted more heavily and gives up proportionally more space. This is why `flex-shrink: 0` ("never shrink this item") is a much more common and predictable production pattern than tuning shrink ratios by hand.
CSS Grid: solving two axes at once
Grid answers a question Flexbox structurally cannot: "place this item at row 2, spanning columns 1 through 3." `display: grid` on a container creates an explicit set of row tracks and column tracks, separated by numbered grid lines — an n-column grid has n+1 vertical lines. `grid-template-columns` and `grid-template-rows` define the tracks; the `fr` unit (a "fraction" of remaining space, conceptually similar to `flex-grow` but for tracks) and `repeat()` make common patterns concise:
.dashboard {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 4 equal columns */
grid-auto-rows: 80px; /* height of any row the browser creates automatically */
gap: 8px;
}
Items are placed either explicitly, with `grid-column` / `grid-row` naming start and end lines (`grid-column: 2 / 4` spans from line 2 to line 4, i.e. two column tracks) or a `span` count (`grid-column: span 2`), or automatically, by an auto-placement algorithm that walks the grid in document order and drops each unplaced item into the first cell area big enough to hold it. `grid-template-areas` offers a third, more readable option for whole-page skeletons — naming rectangular regions as ASCII art:
.page {
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: 64px 1fr 48px;
grid-template-areas:
"nav header"
"nav main"
"nav footer";
min-height: 100vh;
}
.nav { grid-area: nav; }
.header { grid-area: header; }
.main { grid-area: main; }
.footer { grid-area: footer; }
For responsive card grids without a single media query, `repeat(auto-fit, minmax(220px, 1fr))` tells the browser to fit as many 220px-minimum columns as the container allows, then stretch them evenly to fill any remainder — this single line replaces what used to be three or four breakpoint-specific rules.
Worked example 2: tracing the auto-placement algorithm
Place five widgets on the tournament dashboard grid defined above (4 columns, 8px gap, 80px auto rows), in this document order: a header (A, default 1×1), a live-score panel (B, spans 2 columns), a points-table panel (C, spans 2 columns), a weather widget (D, 1×1), and a squad panel (E, 1×1):
<div class="dashboard">
<div class="header">A</div>
<div class="live-score">B</div>
<div class="points-table">C</div>
<div class="weather">D</div>
<div class="squad">E</div>
</div>
.live-score { grid-column: span 2; }
.points-table { grid-column: span 2; }
By default `grid-auto-flow` is `row sparse`: the browser keeps a cursor that scans left-to-right, top-to-bottom, and — critically — only ever moves forward. Once the cursor passes a cell, that cell is never revisited for a later item, even if it stays empty. Trace it:
A (1×1): cursor at row 1, column 1 — free — placed at r1c1. Cursor advances to r1c2.
B (span 2): from r1c2, columns 2–3 are free — placed at r1c2–3. Cursor advances to r1c4.
C (span 2): from r1c4, only column 4 remains in row 1 (there is no column 5 in a 4-column grid) — does not fit. Cursor moves to row 2, column 1. Columns 1–2 are free — placed at r2c1–2. Cursor advances to r2c3.
D (1×1): from r2c3 — free — placed at r2c3.
E (1×1): from r2c4 — free — placed at r2c4.
Final layout: row 1 is A, B, B, empty — column 4 of row 1 is never filled, because the sparse cursor had already moved to row 2 by the time it failed to place C there and never looks back. Row 2 is C, C, D, E. This "stranded hole" is the single most useful thing to internalize about default Grid auto-placement: it is fast (linear scan) but not space-optimal, and empty cells like r1c4 are a normal, expected outcome, not a bug.
The misconception: justify-content is not "horizontal"
Students consistently write `justify-content: center` expecting it to center items horizontally and `align-items: center` expecting vertical centering — and it works, until someone adds `flex-direction: column` and both properties appear to "swap meaning" for no reason. They have not swapped meaning; they never meant "horizontal" or "vertical" in the first place. `justify-content` always acts along the main axis; `align-items` always acts along the cross axis. When `flex-direction: row` (the default), main = horizontal, so the properties look horizontal/vertical. Flip to `flex-direction: column`, and main becomes vertical — so `justify-content` now centers vertically and `align-items` now centers horizontally:
.stat-bar {
display: flex;
flex-direction: column; /* main axis is now vertical */
justify-content: center; /* centers items VERTICALLY */
align-items: flex-start; /* aligns items to the LEFT */
}
The fix is not a new rule to memorize — it is reading both properties correctly the first time: they are axis-relative, and `flex-direction` decides which physical direction "main" currently points in.
Diagram: the two models side by side
Choosing between them — and combining them
Return to the IRCTC screen. The seat map is a genuine 2-axis problem — a seat's meaning depends on row and column together — so it belongs in Grid, most naturally with explicit `grid-template-columns` (one track per berth position: window, middle, aisle, side-lower, side-upper) and `grid-auto-rows` for coach length. The search toolbar is a genuine 1-axis problem — five controls in a line that need to redistribute width — so it belongs in Flexbox. This is the general rule: reach for Grid when you are placing items on both a row and a column at once (page skeletons, dashboards, image galleries with deliberate spans, seat maps, calendars); reach for Flexbox when you are distributing items along a single line (navbars, button groups, form rows, the individual cards inside one of those grid cells). Real interfaces nest both — the `.dashboard` grid from Worked Example 2 places five panels on a 2D grid, and each panel's internal header row (icon, title, refresh button) is itself a small flex container. Neither model is a superset of the other; they solve different-dimensional problems, and production CSS uses both on the same page, often on the same element's parent and children.
Active recall
Attempt every question before reading its answer.
1. In the `.stat-bar` toolbar (960px container, 10px gap, cards with `flex: 1 200px`, `flex: 2 150px`, `flex: 1 100px`), what are the three rendered widths, and what check confirms the arithmetic?
2. The design team ports the same toolbar to a 720px mobile viewport and decides Win Rate should no longer be emphasized, changing its `flex-grow` from 2 to 1 (so all three cards now share `flex-grow: 1`). Recompute all three widths — not just Win Rate's.
3. A card is given `flex: 0 1 300px` inside a container too narrow to fit it alongside its siblings at full basis. Will it shrink? By how much relative to a sibling with the same `flex-shrink` value but a 100px basis?
4. A container has `display: flex` and three child `div`s with no other CSS at all. Do the children fill the container's width? Why or why not?
5. Take the dashboard grid from Worked Example 2 (A, B-span2, C-span2, D, E in that document order, 4 columns) and add `grid-auto-flow: row dense`. Where does every item end up now, and which specific items move compared to the sparse (default) layout?
6. Using `grid-template-areas`, sketch the CSS for a page with a fixed 220px left sidebar, a header above the main content, and a footer below it — spanning the full width only in the sidebar column being excluded from header/footer.
Worked answers
1. Basis sum = 200+150+100 = 450px. Free space = 960 − 450 − 20 (two 10px gaps) = 490px. Grow-factor sum = 1+2+1 = 4. One grow unit = 490 ÷ 4 = 122.5px. Widths: Matches = 200+122.5 = 322.5px; Win Rate = 150+245 = 395px; NRR = 100+122.5 = 222.5px. Check: 322.5+395+222.5+20(gaps) = 960px ✓.
2. Two parameters changed at once, so all three widths change even though only Win Rate's grow factor was touched. Basis sum is still 450px. Free space = 720 − 450 − 20 = 250px. Grow-factor sum is now 1+1+1 = 3. One grow unit = 250 ÷ 3 = 83.33px. Widths: Matches = 200+83.33 = 283.33px; Win Rate = 150+83.33 = 233.33px; NRR = 100+83.33 = 183.33px. Check: 283.33+233.33+183.33+20 = 720px ✓. Note that Matches and NRR changed too, even though their own `flex` declarations were never touched — grow is always computed relative to the sum of every sibling's grow factor, so any sibling's change ripples through everyone's final width.
3. Yes, it shrinks (`flex-shrink: 1` is non-zero). But the amount is not split evenly by shrink value alone: the browser weights each item by its scaled shrink factor = flex-shrink × flex-basis. The 300px card's scaled factor is 1×300=300; a 100px sibling with the same shrink value has a scaled factor of 1×100=100. The 300px card is weighted three times as heavily and gives up three times as many pixels of negative free space, even though both declared `flex-shrink: 1`.
4. No — with no other declarations, every child has the initial Flexbox values: `flex-grow: 0` (claims no extra space), `flex-shrink: 1`, `flex-basis: auto` (sized to content). Since grow is 0, none of the container's leftover width is distributed, so each child simply hugs its own content width and the row leaves empty space on the right. Filling the row requires an explicit non-zero `flex-grow` on at least one child.
5. `dense` restarts the scan from row 1, column 1 for every item (instead of only moving the cursor forward), so it can backfill holes sparse mode leaves stranded. A and B place identically (r1c1 and r1c2-3 — no hole exists yet for them to backfill). C still can't fit in row 1 (only one free cell, needs two) and still lands at r2c1-2. D, scanning from the origin, now finds row-1 column-4 free and takes it — D moves from r2c3 (sparse) to r1c4 (dense). E then finds row 1 fully occupied, checks row 2, and lands at r2c3 — E moves from r2c4 (sparse) to r2c3 (dense). The empty cell relocates from r1c4 to r2c4. So although only D's and E's cells and the hole's position change, tracing "just D" and stopping would miss that E is also displaced as a direct consequence. A practical caveat: `dense` reorders visual position away from source order, which can put a keyboard-tab or screen-reader user's next stop in a visually unexpected place — it should be reserved for purely decorative reflow, not primary content.
6.
.page {
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: 64px 1fr 48px;
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
}
.sidebar { grid-area: sidebar; }
.header { grid-area: header; }
.main { grid-area: main; }
.footer { grid-area: footer; }
The sidebar occupies the full height of column 1 across all three named rows (repeated in every row of the template-areas string), while header, main, and footer stack only within column 2 — exactly the constraint the question specifies.
Think About It
Think about this: How would you explain modern css: grid and flexbox layouts to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where modern css: grid and flexbox layouts is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting modern css: grid and flexbox layouts to at least 3 other topics you have studied.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind modern css: grid and flexbox layouts, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.