Two Layout Problems That Look Nothing Alike
Suppose your school asks you to build two small pieces of the school website. The first is the weekly timetable: eight periods running across the top, six days (Monday to Saturday) running down the side, and a subject sitting in every cell where a row meets a column. The second is the top navigation bar: your school's logo on the left, and links for "Home", "Academics", "Notice Board", and "Contact" sitting in a single row on the right, evenly spaced.
Both look like "put some boxes in order," so a beginner reaches for the same tool for both. But they are not the same kind of problem. The timetable genuinely needs two dimensions at once — the period number tells you the column, the day tells you the row, and both must line up correctly with every other cell for the grid to make sense. The navbar only ever needs one dimension — items sitting along a single line, left to right. Whether that line wraps onto a second line on a small phone screen is a side effect, not the main structure.
This one distinction — is my layout fundamentally one-dimensional (a line of items) or two-dimensional (rows and columns that must align together) — is the single most useful idea in this chapter. CSS gives you a purpose-built tool for each: Flexbox for one-dimensional layouts, and CSS Grid for two-dimensional ones. Most real pages, including the ones you use every day — the IRCTC seat-selection screen, a UPI app's home tiles, a cricket scorecard — are built by combining both, each doing the job it is best at. That combination is what "complex layouts" means, and it is what this chapter builds towards.
Opting Into a Layout Mode
By default, every HTML element you place on a page obeys normal flow: block-level elements (like <div>, <p>, <h3>) stack vertically, one below the other, each taking the full available width. Inline elements (like <span>, <a>) sit side by side along a line, wrapping like words in a sentence. This is why, before you write any CSS at all, a plain HTML page already looks like a readable (if plain) document.
Flexbox and Grid are ways of telling one specific element, "stop obeying normal flow — instead, arrange your direct children using this new set of rules." You switch a container into one of these modes with a single declaration:
.navbar {
display: flex;
}
.timetable {
display: grid;
}
Two things about this are easy to miss and worth stating precisely, because they cause real bugs later. First, display: flex and display: grid change the layout behaviour of the element's direct children only — grandchildren are unaffected and continue to follow normal flow inside their own parent, unless that parent is separately given display: flex or display: grid too. Second, the flex or grid properties you are about to learn (like justify-content or grid-template-columns) only do anything when set on the container, not on the items inside it — a very common beginner mistake is writing justify-content on a child element and wondering why nothing happens.
Flexbox, Built From the Navbar
Start with plain HTML for the navbar — no layout CSS yet:
<nav class="navbar">
<div class="logo">AICI</div>
<a href="#">Home</a>
<a href="#">Academics</a>
<a href="#">Notice Board</a>
<a href="#">Contact</a>
</nav>
In normal flow, the div (a block element) drops onto its own line, and the four links stack strangely depending on their own display type. It does not look like a navbar at all. One line fixes the arrangement into a row:
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
}
display: flex immediately lines every direct child up in a row, left to right, because a flex container's default flex-direction is row. The direction you lay children out along is called the main axis. The direction perpendicular to it is the cross axis. For the default row direction, the main axis is horizontal and the cross axis is vertical. This vocabulary matters because every alignment property in Flexbox is named for one axis or the other: justify-content always controls spacing along the main axis, and align-items always controls positioning along the cross axis — regardless of which physical direction that happens to be.
justify-content: space-between pushes the first item (logo) to the main-axis start and the last item (Contact) to the main-axis end, distributing any leftover space evenly between the items in the middle. align-items: center centers every item along the cross axis, so a taller logo and shorter text links all line up vertically in the middle of the bar instead of sitting at mismatched heights.
Flexible Sizing: A Worked Example With flex-grow
The most powerful (and most misunderstood) part of Flexbox is how it fills leftover space. Consider a flex container that is exactly 900px wide, holding three items:
.container { display: flex; width: 900px; }
.item1 { flex: 1 1 100px; } /* grow:1 shrink:1 basis:100px */
.item2 { flex: 2 1 100px; } /* grow:2 shrink:1 basis:100px */
.item3 { flex: 1 1 100px; } /* grow:1 shrink:1 basis:100px */
The flex shorthand sets three values: flex-grow, flex-shrink, and flex-basis. Work out the final widths the way the browser does, step by step:
- Start from flex-basis. Each item's starting size is its
flex-basis: 100px, 100px, 100px. That uses up 300px of the container's 900px. - Find the leftover space. 900px − 300px = 600px remains unclaimed.
- Add up the grow factors. 1 + 2 + 1 = 4 total "shares."
- Divide the leftover space into shares. 600px ÷ 4 shares = 150px per share.
- Give each item its share. Item 1 gets 1 share = 150px. Item 2 gets 2 shares = 300px. Item 3 gets 1 share = 150px.
- Add the share back to the basis. Item 1: 100 + 150 = 250px. Item 2: 100 + 300 = 400px. Item 3: 100 + 150 = 250px.
Check the total: 250 + 400 + 250 = 900px — exactly the container width, as it must be.
Common misconception, corrected with these numbers: students often assume that "item 2 has flex-grow: 2, so item 2 will end up exactly twice as wide as item 1." Look at the real numbers: item 2 is 400px and item 1 is 250px — that is a ratio of 1.6, not 2. The grow factor only decides how the leftover 600px is split, not the final total width. Because all three items started from the same 100px basis, item 2's advantage gets diluted by the basis they share. The "twice as wide" intuition only becomes exactly true when every item's flex-basis is set to 0 — then the entire width is leftover space, and grow factors alone decide the outcome. With flex: 1 0 0, flex: 2 0 0, flex: 1 0 0 on the same 900px container, the split would be exactly 225px, 450px, 225px — now genuinely a 1:2:1 ratio.
Wrapping: What Happens When Items Don't Fit
By default, flex-wrap: nowrap forces every item onto a single line, shrinking them (using flex-shrink) if the total content is wider than the container — which is why a navbar with too many links can look uncomfortably squeezed on a phone. Setting flex-wrap: wrap instead allows items to fall onto additional lines once they run out of room, the way a row of course cards on a results page reflows from four per row on a laptop to one per row on a phone:
.card-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.card {
flex: 1 1 220px; /* grow, shrink, but never go below ~220px */
}
Here, flex-basis: 220px acts like a minimum comfortable width. When the row is wide enough for four 220px+ cards, they sit on one line and grow to fill remaining space. When the screen narrows, cards that no longer fit wrap onto the next line instead of squeezing below a readable width. gap inserts consistent spacing between items and, unlike margins, never adds extra space at the very edges of the container.
CSS Grid, Built From the Timetable
Flexbox is one-dimensional: even when it wraps onto multiple lines, each line manages its own sizing independently, so items in different rows are not guaranteed to line up in neat columns. The timetable needs true alignment across both dimensions — every "Period 3" column must line up exactly for every day. That is exactly the problem Grid is designed to solve.
<div class="timetable">
<div>Monday</div>
<div>Maths</div>
<div>Science</div>
<div>Hindi</div>
<div>Tuesday</div>
<div>English</div>
<div>Computer Science</div>
<div>Maths</div>
</div>
.timetable {
display: grid;
grid-template-columns: 120px repeat(3, 1fr);
gap: 8px;
}
This single declaration does something Flexbox cannot: it defines a fixed set of column tracks that every row will share. 120px reserves a fixed width for the day-name column; repeat(3, 1fr) creates three more columns, each getting an equal fraction of whatever width is left over. With eight items and four columns, the grid automatically wraps to two rows, and — crucially — the four columns in row 2 line up perfectly under the four columns in row 1, because they were never independent lines the way flex-wrap lines are. That automatic, guaranteed alignment across rows is the entire reason Grid exists.
The fr Unit: A Worked Example (and a Second Misconception)
The unit fr stands for "fraction," and it behaves a lot like flex-grow — but over leftover space in a track list, not over flex items. Take this container:
.layout {
display: grid;
grid-template-columns: 120px 1fr 2fr;
gap: 20px;
width: 760px;
}
Work out each column's rendered width exactly as the browser does:
- Subtract the gaps first. Three columns have two gaps between them: 2 × 20px = 40px. 760px − 40px = 720px remains for the actual columns.
- Subtract fixed-size tracks. The first column is a fixed
120px. 720px − 120px = 600px remains for thefrtracks. - Add up the fr units. 1fr + 2fr = 3 total shares.
- Divide the leftover 600px into 3 shares. 600px ÷ 3 = 200px per share.
- Assign shares. The
1frcolumn gets 1 × 200px = 200px. The2frcolumn gets 2 × 200px = 400px.
Verify: 120 + 200 + 400 + 40 (gaps) = 760px, matching the container exactly.
Second common misconception, corrected with these numbers: students often assume 1fr simply means "one-third of the container" whenever there are three columns, giving 760px ÷ 3 ≈ 253px. That is wrong here — the real answer is 200px, not 253px — precisely because fr only divides what is left over after fixed-width tracks (the 120px column) and gaps (40px total) are subtracted first. Just like flex-grow, fr is a share of leftover space, never a share of the total.
Naming Regions With grid-template-areas
For page-level layouts — the kind "complex layouts" really refers to — Grid offers a second, more readable syntax: naming rectangular regions directly. Picture a results-dashboard page with a header, a left sidebar of filters, a main results panel, and a footer:
.dashboard {
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: 70px 1fr 50px;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
gap: 12px;
min-height: 100vh;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }
Each quoted string in grid-template-areas is one row of the grid, and each word inside it names the area that occupies that cell. Because "header header" repeats the same name across both columns, the header automatically spans the full width — no separate grid-column: span 2 needed. A child is placed by matching its grid-area name to the name in the map, so the CSS reads almost like a drawing of the page. This is far easier to review and modify than remembering which numbered row and column line every element starts and ends on, especially once a layout has more than four or five regions.
Combining Grid and Flexbox: How Real Interfaces Are Actually Built
Production interfaces almost never use only one of these tools. The professional pattern is: use Grid for the page skeleton — the macro, two-dimensional arrangement of major regions — and use Flexbox inside individual regions for one-dimensional alignment of their contents. A UPI payment app's home screen is a clean example: the overall screen (balance card, quick-pay grid of contact icons, recent transactions list) is a Grid; but inside the balance card, the bank logo, balance text, and "Add Money" button sitting in a single row are Flexbox.
.dashboard { display: grid; grid-template-columns: 220px 1fr; grid-template-areas: "sidebar main"; }
.main { grid-area: main; }
.stat-card {
display: flex; /* Flexbox INSIDE a Grid cell */
align-items: center;
justify-content: space-between;
padding: 16px;
}
Notice .main is positioned by Grid (it fills the main area of the outer skeleton), while .stat-card, a child living inside .main, is its own independent flex container arranging an icon, a number, and a label along one line. Grid and Flexbox nest inside each other freely because display: flex and display: grid only ever govern an element's own direct children — a rule stated earlier that now pays off directly.
Responsive Columns Without Media Queries: auto-fit and minmax()
A common complex-layout need is a card grid that automatically shows more columns on a wide screen and fewer on a narrow one, without writing a separate media query for every breakpoint:
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}
minmax(220px, 1fr) tells each column "never go below 220px, but grow to fill available space above that." repeat(auto-fit, …) tells the browser to fit as many such columns as possible. Trace the arithmetic for a container exactly 940px wide: each column needs at least 220px, and each additional column also costs a 20px gap. Fitting n columns needs 220n + 20(n − 1) ≤ 940. Try n = 4: 220×4 = 880, plus 3 gaps × 20 = 60, total 940 — it fits with exactly zero pixels spare. Try n = 5: 220×5 = 1100, already over 940 before gaps are even added. So exactly 4 columns fit, each rendered at exactly 220px, because there is no leftover space for the 1fr half of minmax to distribute.
Now widen the same container to 1200px. n = 5 needs 220×5 + 20×4 = 1100 + 80 = 1180px — that fits, with 1200 − 1180 = 20px left over. n = 6 needs 220×6 + 20×5 = 1320 + 100 = 1420px — too wide. So the grid settles on 5 columns, and the leftover 20px is distributed across the five equally-weighted 1fr columns: 20px ÷ 5 = 4px extra each, making every column exactly 224px. Check: 5 × 224 + 4 × 20 = 1120 + 80 = 1200px — exact. This is why the same one line of CSS reflows a card grid from 4 comfortable columns to 5 slightly wider ones as the viewport grows, entirely without a media query.
Practice: Predict, Then Verify
- A flex container is exactly 600px wide. Three items share
flex-basis: 0withflex-growvalues 1, 1, and 2. What is each item's final width? (Work it through the six-step method above before checking: total shares = 4, so 600 ÷ 4 = 150px per share, giving 150px, 150px, and 300px.) - True or false: setting
justify-content: centeron a container withflex-direction: columnwill center items horizontally. (False — with a column direction, the main axis is vertical, sojustify-contentcenters items vertically;align-itemswould control the horizontal, cross-axis position instead.) - A grid has
grid-template-columns: 80px 1fr 1fr 1fr,gap: 10px, inside a 830px-wide container. Find the width of one1frcolumn. (Subtract 3 gaps = 30px and the fixed 80px from 830px, leaving 720px for 3 equal fr tracks: 720 ÷ 3 = 240px each.) - Rewrite a page that currently uses
grid-template-areas: "header header" "nav main" "footer footer"so that the navigation column also stretches beneath the footer on very wide screens, spanning all three rows in the first column instead of stopping above the footer. Sketch the new quoted strings before writing any code. - A card row uses
flex-wrap: wrapwith cards atflex: 1 1 260px. On a 375px-wide phone screen, predict how many cards sit per row and why the row does not try to squeeze a card down to fit two per line.
Summary
Choose Flexbox when a layout is genuinely one-dimensional — a single row or column of items whose main job is ordering and spacing along one line, with wrapping as a fallback, not a requirement (navbars, button groups, a card's internal icon-and-text row). Choose Grid when alignment must hold across two dimensions at once — rows and columns that must line up together (timetables, dashboards, page skeletons with header/sidebar/main/footer). Flex's main axis and cross axis flip when flex-direction changes, and justify-content always targets the main axis while align-items always targets the cross axis. Both flex-grow and the fr unit distribute only leftover space after fixed sizes (and, for Grid, gaps) are subtracted — never a proportion of the total — which is the single most common source of "why isn't this the ratio I expected" bugs. grid-template-areas lets a complex page skeleton be written as a readable map rather than numbered line references. And the professional pattern for genuinely complex layouts is not "Grid versus Flexbox" but Grid for the macro skeleton with Flexbox nested inside individual regions for their internal one-dimensional alignment — exactly how real dashboards, UPI apps, and news homepages are actually built.
Think About It
Think about this: How would you explain css grid and flexbox mastery: complex 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.