Layout Systems: Flexbox & Grid
CSS layout isn't a single system β it's a stack of formatting contexts. Senior engineers choose the right layer for each problem: normal flow for documents, Flexbox for one-dimensional distribution, Grid for two-dimensional placement.
Normal Flow & Block Formatting Context
Normal flow is the default. Block elements stack vertically, inline elements flow horizontally. A Block Formatting Context (BFC) is an isolated layout region β floats, margins, and overflows are contained within it.
/* Creating a BFC */
.container {
display: flow-root; /* Modern, explicit BFC */
}
/* These also create a BFC (as a side effect) */
.overflow { overflow: hidden; }
.flex { display: flex; }
.grid { display: grid; }
.absolute { position: absolute; }BFC matters because it prevents margin collapsing across boundaries and contains floats. If two vertical margins touch within the same BFC, they collapse. A new BFC prevents this.
Flexbox: One-Dimensional Layout
Flexbox distributes space along a single axis. The mental model is two axes:
Main Axis (justify-content)
βββββββββββββββββββββββββββ
β [ Item 1 ] [ Item 2 ] [ Item 3 ]
β
β Cross Axis (align-items)
βThe Flex Shorthand
flex is a shorthand for flex-grow, flex-shrink, and flex-basis:
.item {
flex: 1; /* flex: 1 1 0% β grow equally from zero base */
flex: 0 0 200px; /* fixed 200px, no grow, no shrink */
flex: 2 1 auto; /* grow at 2x rate, shrink normally, base = content */
}flex value | grow | shrink | basis | Behavior |
|---|---|---|---|---|
flex: initial | 0 | 1 | auto | Don't grow, can shrink |
flex: auto | 1 | 1 | auto | Grow and shrink from content size |
flex: none | 0 | 0 | auto | Rigid β no flex at all |
flex: 1 | 1 | 1 | 0% | Ignore content size, share space equally |
flex: 2 | 2 | 1 | 0% | Get twice the share of flex: 1 items |
Common Flexbox Patterns
/* Centering β the classic */
.center {
display: flex;
justify-content: center;
align-items: center;
}
/* Sticky footer */
body {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
main { flex: 1; }
/* Space between with auto margin */
.nav {
display: flex;
align-items: center;
gap: 1rem;
}
.nav .logo { margin-right: auto; }The gap Property
gap works in both Flexbox and Grid. It replaces margin-based spacing hacks:
/* Old approach β margin on children with negative margin on parent */
.old-list { margin: -8px; }
.old-list > * { margin: 8px; }
/* Modern approach */
.new-list { display: flex; gap: 16px; flex-wrap: wrap; }Grid: Two-Dimensional Layout
Grid controls both rows and columns simultaneously. Think of it as laying items on a coordinate system.
col 1 col 2 col 3
ββββββββββ¬βββββββββ¬βββββββββ
row 1β header β
ββββββββββΌβββββββββΌβββββββββ€
row 2β sidebarβ main β
ββββββββββΌβββββββββΌβββββββββ€
row 3β footer β
ββββββββββ΄βββββββββ΄βββββββββGrid Tracks, Lines, and Areas
.layout {
display: grid;
grid-template-columns: 250px 1fr 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header header"
"sidebar main main"
"footer footer footer";
gap: 1rem;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }The fr Unit
fr distributes remaining space after fixed tracks are sized:
.grid {
grid-template-columns: 200px 1fr 2fr;
/* 200px fixed, then remaining space split 1:2 */
}minmax() β Flexible Bounds
.grid {
grid-template-columns: minmax(200px, 1fr) 3fr;
/* First column: at least 200px, at most 1fr of remaining */
grid-template-rows: minmax(100px, auto);
/* Row: at least 100px, expands to fit content */
}auto-fill vs auto-fit
Both create responsive grids without media queries, but they differ when there's extra space:
/* auto-fill: keeps empty tracks β preserves the grid structure */
.fill {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
/* auto-fit: collapses empty tracks β items expand to fill */
.fit {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}With 3 items in a 1000px container with 200px minimum:
- auto-fill: Creates 5 columns (200px each), 3 filled + 2 empty
- auto-fit: Creates 3 columns that expand to ~333px each
Grid Line Placement
.item {
grid-column: 1 / 3; /* From line 1 to line 3 (spans 2 columns) */
grid-column: 1 / -1; /* From first to last line (full width) */
grid-column: span 2; /* Span 2 columns from auto-placement */
grid-row: 2 / span 3; /* Start at row line 2, span 3 rows */
}Intrinsic Sizing
These keywords let content dictate size rather than fixed values:
.element {
width: min-content; /* Shrink as small as possible without overflow */
width: max-content; /* Grow to fit content on one line, no wrapping */
width: fit-content(300px); /* min-content β€ width β€ min(max-content, 300px) */
}| Keyword | Behavior | Use Case |
|---|---|---|
min-content | Smallest size without overflow | Tags, badges |
max-content | Natural content size, no wrapping | Tooltips, dropdowns |
fit-content(x) | Clamps between min-content and x | Responsive elements with max width |
When to Use Flexbox vs Grid
| Criterion | Flexbox | Grid |
|---|---|---|
| Dimensionality | One axis at a time | Two axes simultaneously |
| Content vs layout | Content dictates layout | Layout dictates placement |
| Item sizing | Items negotiate size | Tracks define size |
| Alignment | Per-axis alignment | Cell-level placement |
| Best for | Navbars, cards in a row, form controls | Page layouts, dashboards, galleries |
Rule of thumb: If you're setting flex-wrap and aligning the wrapped rows matters β you probably want Grid.
Positioning and Stacking Context
Position Values
| Value | Behavior | Creates Stacking Context |
|---|---|---|
static | Normal flow (default) | No |
relative | Offset from normal position, stays in flow | Only with z-index |
absolute | Removed from flow, relative to containing block | Only with z-index |
fixed | Relative to viewport | Always |
sticky | Hybrid β normal flow until scroll threshold | Always |
Containing Block
An absolutely positioned element is placed relative to its containing block β the nearest ancestor with position other than static:
.parent {
position: relative; /* Establishes containing block */
}
.child {
position: absolute;
top: 0;
right: 0; /* Top-right corner of .parent */
}Stacking Context and z-index
A stacking context is a 3D layering group. z-index only competes within the same stacking context:
/* These create new stacking contexts */
.a { position: relative; z-index: 1; }
.b { opacity: 0.99; }
.c { transform: translateZ(0); }
.d { isolation: isolate; }
.e { will-change: transform; }Root stacking context
βββ .header (z-index: 100)
β βββ .dropdown (z-index: 999999) β trapped inside .header's context
βββ .modal-overlay (z-index: 200) β beats .dropdown despite lower z-index
β βββ .modal (z-index: 1)The .dropdown at z-index: 999999 cannot escape its parent's stacking context. This is the #1 z-index debugging trap.
Solution: Use isolation: isolate intentionally to create stacking contexts, and keep z-index values in a token system:
:root {
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-modal: 400;
--z-toast: 500;
}sticky Positioning
sticky toggles between relative and fixed based on scroll position:
.table-header {
position: sticky;
top: 0;
background: white;
z-index: var(--z-sticky);
}Why sticky fails silently:
- An ancestor has
overflow: hiddenoroverflow: auto(creates a scroll container boundary) - No threshold is set (
top,bottom,left, orright) - The element's containing block is too short for scrolling to occur
Interview Signal
Senior candidates demonstrate:
- Mental model clarity β Explaining Flexbox as 1D distribution and Grid as 2D placement, not just syntax
- BFC understanding β Knowing what creates a BFC and why it matters for margin collapsing and float containment
frvs percentages β Understanding thatfrdistributes remaining space after fixed tracks, while percentages reference the container- Stacking context mastery β Diagnosing z-index bugs by identifying stacking context boundaries
- Appropriate tool selection β Choosing Grid for 2D layout and Flexbox for 1D content distribution, with clear reasoning