The box model and normal flow.
Before anything is positioned, layered or animated, the browser turns each element into a set of nested rectangles and drops them into the page by two simple rules: blocks stack, text wraps into lines. Most of the spacing surprises in a real app, a card a few pixels too wide, a gap that is 24 instead of 40, a heading that pushes its whole column down, come from those two rules and nothing else.
The idea.
Layout starts by turning elements into rectangles. How big each one is, and where the next one goes, follows from a handful of rules you can do in your head.
Slatework's design file says a task card is 320 pixels wide with 16 pixels of padding and a hairline border. A developer types exactly that into the stylesheet, opens the board, and the card is 354 pixels wide and sticks out of its column. Nothing is broken. The browser did what the declaration asked, because by default width means the width of the content only, and padding and border are added outside it.
That is the whole topic in one bug. Before the browser paints anything it builds a tree of boxes from the elements and their computed styles. Each box has a content area for text and children, then padding, then a border, then a margin that keeps neighbours away. Its final size comes from three inputs: what it contains, what its own properties say, and the width its parent makes available.
Then the boxes are placed. Without flex, grid, floats or positioning, the browser uses normal flow: block-level boxes go one under the other, each as wide as its parent allows; inline content such as text and links runs along a line and wraps onto the next one when it runs out of room. Two quirks of that flow, percentages measured against the parent and vertical margins that merge instead of adding up, explain most of the spacing that does not match the mockup.
Where a box's size comes from
One Slatework card, as boxes
width: 320px sizes the content box only. Padding and border are added outside it, so the border box is 320 + 32 + 2 = 354 pixels and the margin box 378.
content-box (the default). Box layout “article.card”: 3 shapes. View: 412 × 196 from -16, -28. 1. “margin box” (rect) badge “margin: 12px” 378 × 146 at 0, 0 yellow 2. “border box” (rect) badge “1px border + 16px padding” 354 × 122 at 12, 12 green in margin box 3. “content box” (rect) badge “width: 320px” says “Fix flaky login test” 320 × 88 at 29, 29 blue in border box
- What the reader sees as the card2TaskCard
Where the pixels of one card go, across the inline axis
- width 12, Margin, value 12
- width 1, Border
- width 16, Padding, value 16
- width 320, Content, value 320 (the declared width)
- width 16, Padding, value 16
- width 1, Border
- width 12, Margin, value 12
- border box 354, from 12 to 366
- width 12, Margin, value 12
- width 1, Border
- width 16, Padding, value 16
- width 286, Content, value 286
- width 16, Padding, value 16
- width 1, Border
- width 12, Margin, value 12
- border box 320 = width, from 12 to 332
- Margin
- Border
- Padding
- Content
How it works.
Four steps, in the order the browser applies them. display decides which boxes exist, box-sizing and the containing block decide their size, normal flow places them, and margin collapsing adjusts the space between them.
display has two halves
It is tempting to read display: flex as a description of the element itself. It is really two answers packed into one word. The outer display type says how this box behaves among its siblings: a block-level box takes a line of its own across the parent; an inline-level box sits in a line of text next to its neighbours. The inner display type says how this box arranges its own children: flow (ordinary block and inline layout), flow-root (the same, but sealed off from the outside, which the next topic explains), flex, grid or table.
CSS Display Level 3 lets you write both halves, so display: inline flex is a box that sits in a line of text and lays out its children as a flex row. The familiar single keywords are short forms of the pairs: block means block flow, inline-block means inline flow-root, flex means block flex. Every current engine accepts the two-word form (Chrome since 115, Firefox since 70, Safari since 15), but for everyday layout it adds nothing the single keywords cannot say, so most codebases keep them.
Two values produce no box of their own. display: none removes the element and its whole subtree from layout. display: contents removes only the element's own box and promotes its children, as if the wrapper were not there; it is handy for a wrapper a framework forces on you, but check how the element is exposed to assistive technology, which has had browser bugs. The render pipeline topic covers how this box tree is built from the DOM and styles.
The keywords you use, split into their two halves
| You write | Outer (among siblings) | Inner (for its children) | What you get in Slatework |
|---|---|---|---|
| block | block | flow | A task card: takes the full width of its column, children stack inside |
| inline | inline | flow | A link or an @mention inside a comment: flows with the text |
| inline-block | inline | flow-root | The QA tag chip if you want it to keep its own height and padding in the line |
| flow-root | block | flow-root | A column that must contain its children's margins and floats |
| flex | block | flex | The card's meta row: avatar, comment count and due date side by side |
| inline-flex | inline | flex | A button with an icon and a label, sitting in a sentence |
| grid | block | grid | The board: columns side by side |
| contents | (no box) | children promoted | A wrapper div the component library adds around list items |
| none | (no box) | (no box) | A collapsed filter panel: nothing laid out, nothing announced |
A card's elements and the boxes they generate
4 steps. Step through to see what one change does to these components.
Two directions, named by the text
Normal flow moves in two directions, and CSS names them after the writing rather than the screen. The inline direction is the way a line of text runs. The block direction is the way lines, paragraphs and block boxes pile up. In English both are the familiar left-to-right and top-to-bottom; in Arabic the inline direction runs right to left while blocks still stack downwards; in vertical Japanese the lines run top to bottom and stack from right to left.
This matters here for two reasons. Block boxes fill the inline size of their parent and stack in the block direction, whatever the language. And margin collapsing, below, happens only along the block axis, so in a vertical writing mode it is the left and right margins that merge. The flow-relative property names (margin-block, padding-inline, inline-size) simply name the sides of the box this way; when to use them across a product is the right-to-left topic's lesson.
The same two axes in three writing modes
| Writing mode | Inline direction (a line runs) | Block direction (blocks stack) | Margin that merges with the next block |
|---|---|---|---|
| horizontal-tb, English | left to right | top to bottom | margin-bottom |
| horizontal-tb, direction: rtl, Arabic | right to left | top to bottom | margin-bottom |
| vertical-rl, Japanese set vertically | top to bottom | right to left | margin-left |
How a block box gets its size
A block box in normal flow is measured against its containing block, which for a box in flow is the content box of its nearest block container ancestor. (Positioned boxes pick a different one; that is the positioning topic.) Across the inline axis the parts have to add up exactly: left margin, left border, left padding, width, right padding, right border and right margin together equal the containing block's width. Leave width as auto and it soaks up whatever is left, which is why a plain div fills its parent. Give it a width and set both inline margins to auto, and the two margins share the leftover equally: the box is centred.
Height works the other way round. An auto height is the height of whatever the box contains, so content decides it after the width is known. That is also why a percentage height often does nothing: if the parent's own height depends on its content, there is nothing fixed to take a percentage of, and the browser treats it as auto. Percentage widths always have something to measure against. A less obvious rule: percentage padding and margin are measured against the containing block's width on all four sides, top and bottom included, which is the old trick behind boxes that keep their shape (aspect-ratio does it directly today).
One Slatework column hands its cards 336 pixels
- Column width
- 360 px
- Column padding
- 0 12pxno padding at the top or bottom yet
- Card padding and border
- 16 px and 1 px
- The cards' containing block (the column's content box)360 − 12 − 12336 pxfrom Column width and Column padding
- Card with width: auto336 − 2 × 1 − 2 × 16302 px of content; the border box is exactly 336from The cards' containing block (the column's content box) and Card padding and border
- Card with width: 50% (content-box)50% × 336, then + 34168 px of content, 202 px border boxfrom The cards' containing block (the column's content box)
- padding: 5% on the card5% × 336, on every side16.8 px top, right, bottom and leftfrom The cards' containing block (the column's content box) · The top and bottom padding come from the width too, not from any height.
- height: 50% on the cardcolumn height is auto (it grows with its cards)treated as height autofrom Column width
- width: 280px; margin-inline: auto (border-box)(336 − 280) ÷ 228 px of margin on each sidefrom The cards' containing block (the column's content box)
- A card that is told nothing about its width is the right width: it fills the 336 its column offers.
- Percentages follow the parent's width, so changing the column's padding changes every percentage inside it.
Inline content and line boxes
Inside a block whose children are inline, the browser lays the content out in an inline formatting context. It fills a line box, a strip as wide as the block's content box, with words, links and chips one after another along the inline direction. When the next piece does not fit, it breaks at an allowed spot, usually a space, and starts a new line box under the first one. An inline element that does not fit is cut into pieces, one per line, each with its own share of the background. The block's height is then the line boxes stacked.
Inline boxes do not get a say in that height. width and height are ignored on a plain inline element such as a span. Horizontal padding, border and margin push neighbours along the line. Vertically, margins do nothing and padding and border only paint: the chip's background grows up and down and may overlap the line above, while the lines stay where line-height put them. When a piece of the line needs real height, make it inline-block (or inline-flex): it is then an atomic box that the line has to make room for.
One more surprise belongs here. A word with no break opportunity, such as a long URL pasted into a card title, cannot be split, so it overflows the line box sideways. overflow-wrap: anywhere lets the browser break it where it must.
A wrapped card title, as line boxes
The chip's 4 pixels of vertical padding paint above and below its line, overlapping line 2, but every line box is still 24 tall and the title still 72.
Inline chip. Box layout “h3.card-title”: 5 shapes. View: 318 × 140 from -16, -28. 1. “h3 block box” (rect) badge “line-height: 24px” 286 × 72 at 0, 0 blue 2. “line box 1” (rect) says “Fix flaky login test on the” 286 × 24 at 0, 0 sand in h3 block box 3. “line box 2” (rect) says “staging runner before the” 286 × 24 at 0, 24 sand in h3 block box 4. “line box 3” (rect) says “Friday release” 286 × 24 at 0, 48 sand in h3 block box 5. “span.tag” (rect) badge “inline, padding 4px” says “QA” 40 × 32 at 128, 44 orange in line box 3
What each outer type lets you set
| Property | block | inline (a span) | inline-block |
|---|---|---|---|
| Starts on a new line | yes | no | no |
| Default inline size | fills the containing block | its content | its content (shrink to fit) |
| width and height | apply | ignored | apply |
| Vertical padding and border | push other boxes | paint only | push the line apart |
| Vertical margin | spaces blocks, can collapse | no effect on layout | spaces it in the line, never collapses |
Margins that merge
Padding and border always add up. Vertical margins in block layout often do not. When two block-axis margins touch with nothing between them, the browser keeps one margin the size of the larger and throws the other away. The rule was made for text: a paragraph with 1em of space above and below should be 1em from the next paragraph, not 2em. It still applies to every block in normal flow, including cards, sections and headings, and it is the single most common reason a gap on screen does not match the numbers in the stylesheet.
Margins touch in three situations. Two siblings, one under the other: the bottom margin of the first meets the top margin of the second. A parent and its first child: if the parent has no top border, no top padding and no inline content before the child, the child's top margin and the parent's top margin are the same edge, and the merged margin ends up outside the parent. The same goes for a parent and its last child at the bottom, where a set height or min-height on the parent also keeps them apart. And an empty block: with no content, padding, border or height, its own top and bottom margins meet, and it can collapse through to join the margins on either side of it.
Negative margins join in by arithmetic. The result is the largest positive margin plus the most negative one; if every margin is negative, the most negative wins. Anything that puts something between the two edges stops the merge: a border, padding, a line of text. So does a parent that starts its own block formatting context (display: flow-root is the clean way; the next topic lists the others), and so does leaving normal flow altogether. Flex items and grid items never collapse, and neither do floats or absolutely positioned boxes. Inline-axis margins never collapse in any layout.
Two cards, two margins, one gap
Both margins start at A's bottom edge and overlap. The larger one wins, so B starts 24 below A, not 40.
Block column, gap 24. Box layout “div.column”: 5 shapes. View: 368 × 296 from -16, -28. 1. “div.column” (rect) badge “display: block” 336 × 240 at 0, 0 sand 2. “card A” (rect) badge “margin-bottom: 16px” says “Fix flaky login test” 336 × 96 at 0, 0 blue in div.column 3. “A's margin, 16” (rect) 160 × 16 at 0, 96 yellow in div.column 4. “B's margin, 24” (rect) 160 × 24 at 176, 96 yellow in div.column 5. “card B” (rect) badge “margin-top: 24px” says “Update the release notes” 336 × 96 at 0, 120 blue in div.column
- The column decides whether margins merge1BoardColumn
When block margins collapse, and what keeps them apart
| Case | Margins that merge | Kept apart by |
|---|---|---|
| Adjacent siblings | margin-bottom of one + margin-top of the next | Anything in between that is not empty, clearance past a float, or the parent being flex or grid |
| Parent and first child | parent margin-top + child margin-top | Parent's border-top or padding-top, inline content before the child, clearance, or the parent starting its own block formatting context |
| Parent and last child | parent margin-bottom + child margin-bottom | Parent's border-bottom or padding-bottom, a set height or min-height, or its own block formatting context |
| Empty block | its own margin-top + margin-bottom | Any content, padding, border, height or min-height |
| Never | inline-axis margins; floats; absolutely positioned boxes; flex and grid items | Not applicable: these always add |
Work these out before you open them
In practice.
Three bugs from the Slatework board, each traced to one rule from the section before, then how to bring the box model into an interview answer and where the system flows lean on it.
The Slatework board
Every column is the same width and hands its cards the same inline size; every card's height comes from its content.
As designed. Board: 7 items. Search: “Filter cards”. Count: 7 cards. Column “To do”: 1. Fix flaky login test · Due Fri by Ines R. badges: QA. 2. Update the release notes · Due Mon by Tomas K. Column “Doing”: 1. Dark mode for settings · Due Wed by Hana O. badges: Design. 2. Cache avatars on the board by Ines R. Column “Review”: 1. Retry failed uploads · Due Thu by Tomas K. 2. Rename the beta flag by Hana O. Column “Done”: 1. Ship the dark theme by Theo B. badges: Design.
- BoardColumn hands each card its width1BoardColumn
- TaskCard sizes to its content2TaskCard
Bug 1: the column's heading pushes the whole column down
Each column has a tinted background and side padding only. Its h2 has a 20 pixel top margin so the title does not touch the edge. On screen the tint starts 20 pixels lower than the other panels and the title sits flush against the top of the tint. The heading's margin has merged with the column's own top margin, because nothing (no padding, no border, no text) separates the two edges, and the merged margin is drawn outside the column, where the background does not reach.
The h2's margin ends up outside its column
The column's top edge and the heading's top margin touch, so they collapse into one 20 pixel margin outside the column. The tint starts 20 pixels low and the title sits on its top edge.
Margin escapes. Box layout “main.board”: 4 shapes. View: 416 × 308 from -16, -28. 1. “main.board” (rect) 384 × 264 at 0, 0 gray 2. “h2's 20px margin” (rect) 200 × 20 at 24, 24 yellow in main.board 3. “section.column” (rect) badge “padding: 0 12px” 360 × 200 at 12, 44 sand in main.board 4. “h2, margin-top: 20px” (rect) says “To do” 336 × 28 at 24, 44 purple in section.column
- BoardColumn1BoardColumn
The column, before and after
.column {
background: var(--column-tint);
padding: 0 12px; /* no top padding: nothing between the edges */
}
.column > h2 {
margin-top: 20px; /* merges with .column's top margin */
}
.column {
background: var(--column-tint);
padding: 12px; /* the margin now has something to push against */
}
.column > h2 {
margin-top: 20px; /* unchanged; it now stays inside the tint */
}
/* Or leave the padding alone and give .column display: flow-root */
Bug 2: a 320 pixel card that is 354 pixels wide
This is the opening bug. The card was given width: 320px, padding: 16px and a 1 pixel border under the default content-box sizing, so its border box is 354. The column is 360 wide with 12 pixels of padding on each side, which leaves 336 for the cards. The card sticks out 18 pixels past that content edge and 6 past the column itself, and in the right-most column that overflow can give the whole board a sideways scrollbar.
Switching the card to border-box makes the number in the stylesheet the number on screen: 320. The better fix is to stop telling the card its width at all. A block box with width auto fills its containing block exactly, so the card becomes 336 wide in this column and still fits if the column is resized, or the board is opened on a phone where columns are narrower.
The card against its column
The declared 320 is only the content. With padding and border the card is 354 wide and its right edge is at 366, past the column's 360.
content-box, 354. Box layout “section.column”: 2 shapes. View: 400 × 208 from -16, -28. 1. “section.column” (rect) badge “360 wide, 336 inside” 360 × 156 at 0, 0 sand 2. “article.card” (rect) badge “320 + 32 + 2 = 354” says “Fix flaky login test” 354 × 120 at 12, 12 orange in section.column
- TaskCard2TaskCard
Two lines that fix it for the whole app
*, *::before, *::after {
box-sizing: border-box;
}
.card {
/* no width: fill the column's content box */
padding: 16px;
border: 1px solid var(--card-border);
}
Bug 3: the gap between cards doubles after a refactor
Cards were spaced with margin-block: 12px, and in a plain block column the bottom margin of one card and the top margin of the next collapsed into a single 12 pixel gap. Then someone made the column a flex container, to pin an 'Add card' button to its bottom. Nothing about the cards changed, yet every gap became 24 pixels. Flex items never collapse margins, so each card's bottom margin and the next card's top margin now add up instead of merging.
The fix is to give the job of spacing to the column. In a flex or grid container, gap puts a fixed space between items and none before the first or after the last, so the spacing no longer depends on which layout mode the parent happens to use. (gap does nothing in plain block layout; there, a margin on the children or the owl-style selector .column > * + * is still the tool.)
Spacing owned by the child, then by the parent
.column { display: flex; flex-direction: column; }
.card { margin-block: 12px; } /* collapsed to 12 in block layout; adds to 24 here */
.column { display: flex; flex-direction: column; gap: 12px; }
.card { margin: 0; }
Using it in an interview answer
What a strong answer says about layout, in a sentence each
Where the system flows lean on it
Trade-offs.
The defaults of normal flow were designed for documents. An app like Slatework mostly overrides them, and each override has a cost worth naming.
- Pro:The size in the stylesheet is the size on screen, so design specs map one to one
- Pro:Changing padding never changes a component's outer size or breaks a row of them
- Pro:Percentages and padding mix safely (width 50% with padding stays half)
- Con:Third-party widgets written for content-box can shift when the reset reaches them
- Con:Content size is now derived, and is floored at zero when padding plus border exceed the width
Every width needs padding and border added in your head; width: 100% plus any padding overflows the parent
- Pro:One number, in one place, between items only, never before the first or after the last
- Pro:Unaffected by margin collapsing, so it reads the same in every layout mode that supports it
- Pro:Children stay reusable: a card carries no outside spacing into the next context
- Con:Needs the parent to be a flex or grid container; plain block layout ignores gap
- Con:Uneven spacing (more room before a section heading) needs an extra rule anyway
The same margins give different gaps in block and flex parents; First and last children leak margin through a parent with no padding or border; Every reusable component brings outside spacing that the next layout has to cancel
- Pro:Fills any column it is dropped into, on desktop or a phone
- Pro:Nothing has to add up across components, so nothing overflows when one value changes
- Con:The card's final width is not visible from its own stylesheet
- Con:Very wide containers need a cap to keep lines readable
Has to agree with every container's padding and the box-sizing in force; Breaks the first time a column, a sidebar or the viewport gets narrower
Things people believe about boxes, and what is actually true
| Belief | What actually happens |
|---|---|
| width: 100% means the box fills its parent | Only under border-box and with no inline margins. Under content-box the padding and border are added on top and it overflows. width auto fills without any of that. |
| Margins always add up | They add along the inline axis, between flex or grid items, and wherever padding or a border separates them. Block-axis margins that touch in normal flow merge. |
| Padding-top: 10% is 10% of the height | Percentage padding and margin are taken from the containing block's width on every side. |
| height: 100% makes a child as tall as its parent | Only if the parent has a definite height. If the parent's height comes from its content, the percentage acts as auto. |
| A span with padding makes its line taller | Vertical padding on an inline box only paints. Make it inline-block to give it height in the line. |