CSS Layout
CSS layout is one of those topics that looks simple on the surface but reveals surprising depth the moment you try to build something real. Why does my margin: auto not center this element? Why did my absolutely positioned child escape its container? Why won't z-index work?
The answer, almost always, is that you haven't fully met the layout model you are working with. This post walks through every major CSS layout mechanism — from the document's default flow all the way to modern Grid — and shows you what each one actually does.
#Normal Flow
Before you add a single layout property, the browser already has opinions about where things go. This is normal flow (also called the in-flow layout model), and understanding it is the prerequisite for understanding everything else.
In normal flow, elements are divided into two formatting contexts:
- Block formatting context — elements stack vertically, one per "line", filling the full width of their container.
<div>,<p>,<h1>participate here by default. - Inline formatting context — elements sit side-by-side along a text baseline, wrapping when they run out of space.
<span>,<a>,<strong>live here.
/* no layout properties needed — this IS normal flow */
div { display: block; } /* default */
span { display: inline; } /* default */
The key insight: any layout property you add — flex, grid, position — is a deliberate departure from this well-defined default. You pull elements out of flow, or you replace the flow algorithm with a different one.
<!-- These two divs stack vertically in block flow -->
<div class="block-a">Block A</div>
<div class="block-b">Block B</div>
<!-- These spans sit side-by-side in inline flow -->
<span>Hello</span>
<span> world</span>
Sidenote: Block elements stack vertically and expand to fill container width. Inline elements sit on the text baseline and wrap. On mobile both become full-width for readability.#The Box Model
Every element in CSS is a rectangular box made of four nested layers. Knowing their names — and their sizes — is crucial for any measurement or positioning work.
The box-sizing property changes what width and height refer to:
/* default — width = content only */
.content-box {
box-sizing: content-box; /* default */
width: 120px;
padding: 16px;
border: 2px solid;
/* total = 120 + 32 + 4 = 156px */
}
/* border-box — width = content + padding + border */
.border-box {
box-sizing: border-box; /* preferred globally */
width: 120px;
padding: 16px;
border: 2px solid;
/* total = 120px — no mental arithmetic */
}
Sidenote: The four layers from inside out: content (your actual text or image), padding (transparent breathing room), border (the visible edge), margin (transparent space that pushes neighbouring elements away). Try the presets to see how each layer enlarges the total footprint.box-sizing: border-box is the universally recommended baseline. Almost every modern project resets to it:
*, *::before, *::after {
box-sizing: border-box;
}
Margin Collapsing
One counterintuitive consequence of block formatting context: adjacent vertical margins collapse. If element A has margin-bottom: 24px and element B directly below it has margin-top: 16px, the gap between them is 24px, not 40px. The larger margin wins; the smaller one is absorbed.
h2 { margin-bottom: 24px; }
p { margin-top: 16px; }
/* gap between h2 and p = max(24, 16) = 24px
not 24 + 16 = 40px */
Sidenote: Margin collapse only happens in the vertical direction, only between elements in the same block formatting context, and never with flex or grid children. It's not a bug — it prevents paragraph text from double-spacing between headings and body copy.#Flexbox
Flexbox is a one-dimensional layout algorithm. You pick an axis — row or column — and items are placed along it. The container controls how leftover space is distributed and how items align on the cross axis.
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: nowrap;
}
The mental model that prevents confusion:
Main axis → controlled by flex-direction
(row = horizontal, column = vertical)
Cross axis → the perpendicular direction
justify-content → distributes space on the MAIN axis
align-items → aligns items on the CROSS axis
Sidenote: Toggle the four core properties to see their effect in real time. The generated CSS block at the bottom reflects your exact selection. Note how align-items: stretch grows all items to equal height — handy for card grids.Flex Items: flex-grow, flex-shrink, flex-basis
The magic of flexbox is that items can grow to fill space or shrink to avoid overflow. These three properties — usually written in shorthand — describe that behaviour:
/* shorthand: flex: grow shrink basis */
.item { flex: 1 1 0%; } /* grow, shrink, start from 0 */
.item { flex: 0 0 200px; } /* rigid, always 200px */
.item { flex: 2; } /* grow twice as fast as flex: 1 siblings */
/* equal-width columns that fill the container */
.nav-item { flex: 1; }
/* one item uses minimum space, the other takes the rest */
.icon { flex: 0 0 40px; } /* always 40px */
.label { flex: 1; } /* fills the remaining space */
Sidenote: flex: 1 is shorthand for flex: 1 1 0% — grow to fill space, shrink if needed, start from zero. It's the most common single value and turns all siblings into equal-width columns.#CSS Grid
Grid is a two-dimensional layout algorithm. Where flexbox works along a single axis, grid manages rows and columns simultaneously. This makes it the right tool for page-level layout, dashboards, and anything that needs true 2D alignment.
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, minmax(40px, auto));
gap: 8px;
}
Two-dimensional alignment is the unique superpower of Grid:
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
/* item spans two columns */
.featured {
grid-column: span 2;
}
/* item placed explicitly by line number */
.sidebar {
grid-column: 3; /* column line 3 → 4 */
grid-row: 2 / 4; /* rows 2 through 4 */
}
Sidenote: Switch between the three presets. Holy grail shows the classic header / sidebar / main / aside / footer pattern that used to require float hacks — it's four lines of CSS with Grid. Masonry-ish demonstrates span to let items occupy multiple tracks.fr Units and minmax()
Grid introduces the fr (fraction) unit, which is only valid inside grid track definitions. One fr means "one share of the available space after fixed-size tracks are placed":
/* 3 equal columns */
grid-template-columns: 1fr 1fr 1fr;
/* sidebar fixed, main flexible */
grid-template-columns: 200px 1fr;
/* responsive without media queries */
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
.card-grid {
display: grid;
/* fill with columns ≥ 180px, stretch to fill row */
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
}
Sidenote: minmax(180px, 1fr) combined with auto-fill is one of the most powerful one-liners in CSS: it creates as many columns as will fit, each at least 180px wide and proportionally stretching to fill the row. No media queries needed.Grid vs Flexbox
A common question: which one should I use?
| Scenario | Reach for |
|---|---|
| Navigation bar, button group (items in a line) | Flexbox |
| Page skeleton (header, sidebar, main, footer) | Grid |
| Card grid with rows and columns aligned | Grid |
| Centering a single element | Either (flex/grid + place-content: center) |
| Unknown number of items that should wrap proportionally | Flexbox with flex-wrap |
| Items that need to align to a shared row baseline | Grid |
In practice you use both together — Grid for the outer shell, Flexbox within each cell.
#Positioning
All the layout models above keep elements in flow. Positioning lets you step outside that flow entirely.
position: static;
}
static → default; offsets have no effect
relative → offset from normal position; space preserved
absolute → relative to nearest positioned ancestor
fixed → relative to viewport; survives scroll
sticky → flows normally then locks to scroll threshold
Sidenote: Click through each positioning scheme. The dashed outline shows where the element would have been in normal flow — notice it disappears for absolute and fixed because those elements leave flow entirely. sticky is the hybrid: it starts in flow and locks to the scroll threshold.The Containing Block
For an absolute element, the reference frame is its containing block — the nearest ancestor whose position is anything other than static. This is a frequent source of bugs:
/* Bug: child is positioned relative to the page, not the card */
.card { position: static; } /* ← this is the problem */
.tooltip { position: absolute; top: 0; right: 0; }
/* Fix: give the parent a non-static position */
.card { position: relative; }
.tooltip { position: absolute; top: 0; right: 0; }
/* Pattern: contain absolutely-positioned children */
.parent {
position: relative; /* no visual change */
}
.child {
position: absolute;
top: 0;
right: 0; /* now anchors to .parent, not the page */
}
Sidenote: position: relative with no offsets is a no-op visually — the element does not move. But it establishes a containing block, which is exactly why it is the standard fix when an absolutely positioned child escapes to the wrong ancestor.Stacking Order and z-index
When non-static elements overlap, the browser uses a predictable stacking order. z-index only has effect on positioned elements (anything other than static) or flex/grid children.
.overlay {
position: fixed;
z-index: 100; /* bring to front */
}
Stacking contexts are the hidden rule: each z-index is local to its stacking context. A child with z-index: 9999 cannot escape a parent stacking context with z-index: 1. Stacking contexts are created by transform, opacity < 1, filter, will-change, and a few other properties — which is why an element sometimes seems stuck behind another despite its large z-index.
#Multi-Column Layout
CSS has a native newspaper-style column module. It is less common than Flex or Grid, but useful for text-heavy content:
.article {
column-count: 2;
column-gap: 2rem;
column-rule: 1px solid #e2e8f0; /* optional divider */
}
/* or set a minimum column width — columns created as needed */
.article {
column-width: 280px;
}
figure, blockquote {
break-inside: avoid;
}
h2 {
column-span: all;
}
Sidenote: Use break-inside: avoid on elements like blockquotes or figures to prevent them from being sliced across a column break. Use column-span: all to let a heading stretch across every column.#A Mental Model for All of CSS Layout
Every layout decision reduces to one question: what formatting context am I in, and what rules does it follow?
| Context | Created by | Axis | Key property |
|---|---|---|---|
| Block | default (display: block) | vertical | margin, width |
| Inline | display: inline | horizontal | line-height, letter-spacing |
| Flex | display: flex | one axis | justify-content, align-items |
| Grid | display: grid | two axes | grid-template-*, gap |
| Positioned | position: absolute/fixed | free placement | top, right, z-index |
When something looks wrong, the first question is always: which context owns this element right now, and am I fighting its rules or working with them?
The cascade, the box model, the positioning scheme, the stacking contexts — they are all consistent, deterministic, and understandable. CSS layout only seems unpredictable until you know the system. Once you do, it is remarkably expressive.