HTML & CSS NotesRohit’s interview study guide
Chapter 03

The Box Model & Sizing

Every element is drawn as a rectangle made of four nested areas; this chapter is how the browser sizes those rectangles, why their margins sometimes merge, how they are hidden, how they handle content that doesn't fit, and which units they are measured in.

9 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.

#The box model: content, padding, border, margin

Added

Everything CSS lays out is a box. Each element generates a rectangle (or none, with display: none), and that rectangle has four layers, from the inside out: the content area (where text and children go), padding (space inside the border, painted with the element's background), the border, and margin (space outside the border that pushes other boxes away and is always transparent).

A good mental model is a framed picture on a wall. The picture is the content. The mat around the picture is the padding: it belongs to the frame and shares its colour. The frame itself is the border. The empty wall you leave between two frames is the margin: it's not part of the frame, nothing is painted there, and it only exists to keep neighbours at a distance.

The one thing that trips people up: by default, width and height set the size of the picture only (the content box). Padding and border are added on top, so a width: 200px box with padding and a border is wider than 200px on screen. The next topic, box-sizing, is the switch that changes that.

Content, padding, border and margin
CSS
.wall {
  display: flow-root; /* keeps the child's margin inside the wall */
  width: 340px;
  background: #f1f5f9;
}
.box {
  width: 200px;
  height: 80px;
  padding: 20px;
  border: 5px solid #2563eb;
  margin: 30px;
  background: #dbeafe;
}
HTML
<div class="wall">
  <div class="box">content: 200 × 80</div>
</div>
Resultlive, in your browser
What’s happening
  1. The content box is exactly what width: 200px; height: 80px asked for, and getComputedStyle(box).width reports 200px.
  2. padding: 20px adds 20px on every side, so the padding box is 240 × 120. That is what clientWidth and clientHeight measure (content plus padding, no border): 240 and 120.
  3. border: 5px adds another 5px per side. The border box, the rectangle you actually see, is 250 × 130: 200 + 2×20 + 2×5 wide and 80 + 2×20 + 2×5 tall. offsetWidth and getBoundingClientRect().width both give 250.
  4. margin: 30px is outside the border: the box starts 30px from the wall's left and top edges, and nothing is painted there (you see the wall's grey through it).
  5. The wall is 190px tall: 130 for the box plus 30 above and 30 below. The wall has display: flow-root so the child's top margin stays inside it; without it, the margin would escape through the wall's top edge (see Margin collapsing).
  6. So the space this box claims in the layout is 310 × 190 (border box plus margins), while its width property says 200. Knowing which of those numbers each API reports is most of the box model.
AdvancedPercent padding and margin use the width

A percentage in padding or margin is always a percentage of the containing block's width, even for padding-top and margin-bottom. That surprises people the first time, and it's the basis of the old aspect-ratio hack (see aspect-ratio).

Vertical percentages measured against the width
CSS
.frame {
  display: flow-root;
  width: 400px;
  height: 100px;
  background: #f1f5f9;
}
.pct {
  padding-top: 10%;
  margin-top: 5%;
  background: #fde68a;
}
HTML
<div class="frame">
  <div class="pct"></div>
</div>
Resultlive, in your browser
What’s happening
  1. .frame is 400px wide and 100px tall. If percentages followed the axis, padding-top: 10% would be 10px.
  2. It computes to 40px instead: 10% of the width (400px). margin-top: 5% computes to 20px, again from the width.
  3. The empty .pct box is therefore 40px tall (all padding) and sits 20px below the frame's top edge.
  4. The spec does this so that padding and margin on all four sides share one reference, which keeps an element's spacing uniform when you write padding: 5%. The side effect is that a vertical percentage scales with width, which is exactly what makes padding-top: 56.25% a 16:9 box.
AdvancedInline boxes ignore vertical spacing

The box model applies fully to block boxes. Inline boxes (a <span>, an <a>) are different: their horizontal padding, border and margin push the text beside them, but vertical padding and border are painted without moving the lines, and vertical margins have no effect at all.

Padding and margin on an inline span
CSS
.line { width: 320px; margin: 0 0 8px; font-size: 16px; line-height: 24px; }
.tag  { background: #fce7f3; border: 2px solid #db2777; }
.tall { padding: 12px 0; margin: 12px 0; }
.wide { padding: 0 12px; margin: 0 12px; }
HTML
<p class="line" id="l1">Before <span class="tag" id="plain">word</span> <span id="a1">after</span></p>
<p class="line" id="l2">Before <span class="tag tall" id="tall">word</span> <span id="a2">after</span></p>
<p class="line" id="l3">Before <span class="tag wide" id="wide">word</span> <span id="a3">after</span></p>
Resultlive, in your browser
What’s happening
  1. All three paragraphs are exactly 24px tall, the line-height. The .tall span's 12px vertical padding and 12px vertical margin did not make its line any taller.
  2. The .tall span's border box is still 24px taller than the plain span's: the padding is painted (the pink background and border grow up and down) but overlaps the lines above and below instead of pushing them.
  3. The .wide span is 24px wider than the plain one (12px padding each side), and its margin-right: 12px moves the following word 12px further away.
  4. This is why padding: 10px on a link in a paragraph looks fine horizontally but overlaps the text above it. To give an inline element real vertical space, make it inline-block (see display).
AdvancedOutline and box-shadow take no space

outline and box-shadow are drawn around the border box but are not part of the box model: they take no space and never move anything. That makes outline the safe choice for focus rings and debug borders, because adding it can't shift the layout.

Border versus outline
CSS
.row { display: flex; gap: 40px; padding: 30px; background: #f1f5f9; }
.row div { width: 60px; height: 40px; background: #dcfce7; }
.with-border  { border: 10px solid #16a34a; }
.with-outline { outline: 10px solid #16a34a; box-shadow: 0 0 0 20px #bbf7d0; }
HTML
<div class="row">
  <div class="with-border"></div>
  <div class="with-outline"></div>
  <div class="next"></div>
</div>
Resultlive, in your browser
What’s happening
  1. The bordered box is 80px wide: 60 of content plus 10 of border on each side.
  2. The outlined box is still 60 × 40: the 10px outline and the 20px shadow are painted outside it, over the gap.
  3. The next box starts exactly 40px (the flex gap) after the outlined box's border edge, so the outline and shadow moved nothing; they simply paint over neighbouring space.
AdvancedMeasuring boxes from JavaScript

Each DOM measurement stops at a different layer, which is a favourite follow-up question:

APIWhat it measuresIn the demo
getComputedStyle(el).widththe width property's used value (content box under the default box-sizing)200px
el.clientWidthcontent + padding (minus any scrollbar), as a whole number240
el.offsetWidthcontent + padding + border, as a whole number250
el.getBoundingClientRect().widththe border box as drawn, as a fraction, after transforms250
el.scrollWidththe full width of the content including the part that overflowssee Overflow

getBoundingClientRect() is the only one that includes transforms (a box scaled to 50% reports half its width) and keeps fractions; the client* and offset* properties round to integers.

How much room does this card take?
CSS
.card { width: 300px; padding: 10px; border: 2px solid; margin: 20px; }

How wide is the card's visible rectangle, and how much horizontal space does it take in the layout?

Show answer

The visible (border) box is 324px wide, and it takes 364px of horizontal space.

The card measured
CSS
.shrink { display: flow-root; width: max-content; background: #f1f5f9; }
.card { width: 300px; padding: 10px; border: 2px solid #475569; margin: 20px; background: #fff; }
HTML
<div class="shrink"><div class="card">card</div></div>
Resultlive, in your browser
What’s happening
  1. Under the default box-sizing: content-box, width: 300px is the content only.
  2. Add padding on both sides (2 × 10) and border on both sides (2 × 2): 300 + 20 + 4 = 324px, the border box the test measures.
  3. Margins sit outside that: 324 + 2 × 20 = 364px. The wrapper uses width: max-content to shrink-wrap the card including its margins, and measures 364.

#box-sizing: content-box vs border-box

Added

box-sizing decides which box width and height describe. The default, content-box, means they size the content only, and padding and border are added outside. border-box means they size the border box, and padding and border are carved out of the inside, so the content shrinks to make room.

In the picture-frame model: content-box is ordering "a 200px picture" and then adding a mat and frame around it, so the whole thing ends up wider. border-box is ordering "a 200px frame" and letting the framer fit a smaller picture inside. When you're dividing a row into columns, the frame size is the one you care about, which is why almost every reset sets border-box on everything.

Historically, content-box is the CSS 1 behaviour, and old Internet Explorer in quirks mode used the border-box model. box-sizing lets you pick either model per element; MDN lists it as Baseline widely available since July 2015.

The same width under both models
CSS
.box {
  width: 200px;
  height: 100px;
  padding: 20px;
  border: 5px solid #7c3aed;
  margin-bottom: 12px;
  background: #ede9fe;
}
.content { box-sizing: content-box; }
.border  { box-sizing: border-box; }
HTML
<div class="box content">content-box</div>
<div class="box border">border-box</div>
Resultlive, in your browser
What’s happening
  1. Both boxes say width: 200px; height: 100px; padding: 20px; border: 5px. Only box-sizing differs.
  2. The content-box box gives the full 200 × 100 to its content and adds padding and border outside: it renders 250 × 150.
  3. The border-box box renders exactly 200 × 100. Padding and border are taken from the inside, so its content area shrinks to 150 × 50 (clientWidth 190 minus 40px of padding).
  4. getComputedStyle().width says 200px for both: it reports the width property, which describes a different box in each case. Read box-sizing before you trust that number.
  5. The practical difference: with border-box, changing padding or border never changes the space the element takes, so layouts stop breaking when someone adds padding: 1rem.
AdvancedWhy border-box makes percentage columns work

The classic failure of content-box: two columns of width: 50% with any padding add up to more than 100%, and the second one no longer fits.

Two 50% columns with padding
CSS
.row { display: flow-root; width: 400px; margin-bottom: 10px; background: #f1f5f9; }
.col { float: left; width: 50%; height: 40px; padding: 10px; border: 2px solid #0891b2; background: #cffafe; }
.row.bb .col { box-sizing: border-box; }
HTML
<div class="row" id="cb"><div class="col">one</div><div class="col">two</div></div>
<div class="row bb" id="bb"><div class="col">one</div><div class="col">two</div></div>
Resultlive, in your browser
What’s happening
  1. Each row is 400px wide, so width: 50% is 200px of content.
  2. Under content-box, each column renders 200 + 2×10 + 2×2 = 224px. Two of them need 448px, more than the row's 400px, so the second float can't fit beside the first and drops below it.
  3. Under border-box, each column renders exactly 200px with the padding and border inside, so both fit and the second starts at the 200px mark.
  4. Before box-sizing was usable, people fixed this with width: calc(50% - 24px) or by nesting a padded inner div. Flexbox and grid avoid the arithmetic altogether, but border-box still matters for every fixed width you write.
AdvancedThe two common resets

box-sizing is not inherited, so setting it on html or body alone does nothing for their children. There are two common resets:

The inherit-based reset
CSS
/* Reset 1, the simple one: */
/* *, *::before, *::after { box-sizing: border-box; } */

/* Reset 2, the one that lets a subtree opt out: */
html { box-sizing: border-box; }
*, *::before, *::after { box-sizing: inherit; }

.legacy { box-sizing: content-box; } /* an old widget that expects content-box */
.w { width: 100px; padding: 10px; height: 20px; background: #fde68a; margin-bottom: 6px; }
HTML
<div class="w" id="normal">app</div>
<div class="legacy">
  <div class="w" id="old">legacy</div>
</div>
Resultlive, in your browser
What’s happening
  1. html gets border-box, and the universal rule makes every element (and its ::before / ::after) inherit its parent's value instead of setting it directly.
  2. The app's .w inherits border-box down the chain from html, so width: 100px renders 100px wide.
  3. .legacy sets content-box on itself, and because its children inherit, the whole subtree switches: the inner .w renders 120px (100 + 2×10 padding). One rule on the widget's root fixes all of it.
  4. With the simple reset (*, *::before, *::after { box-sizing: border-box }), every element sets border-box directly, so an opt-out has to target each element in the widget. Both resets include the pseudo-elements because the universal selector * doesn't match them.
AdvancedEdge case: padding wider than the width

border-box can't make the content area negative. If padding plus border exceed the width, the content box becomes 0 and the element grows to fit the padding and border.

Content can't go below zero
CSS
.tiny { box-sizing: border-box; width: 30px; height: 30px; padding: 0 20px; background: #fecaca; }
.capped { box-sizing: border-box; max-width: 200px; padding: 20px; background: #dcfce7; }
.capped-cb { max-width: 200px; padding: 20px; background: #fef9c3; }
HTML
<div class="tiny"></div>
<div class="capped">capped</div>
<div class="capped-cb">capped</div>
Resultlive, in your browser
What’s happening
  1. .tiny asks for a 30px border box but has 40px of horizontal padding. The content box can't be -10px, so it's 0 and the box renders 40px wide: the width is effectively ignored.
  2. min-width and max-width follow box-sizing too. .capped (border-box) is capped at a 200px border box.
  3. .capped-cb uses the default content-box, so its cap applies to the content and the padding comes on top: 240px.
width: 100% plus padding

A container is 300px wide. Its child has width: 100%; padding: 0 20px and no box-sizing rule. How wide is the child, and does it fit?

Show answer

It renders 340px wide and sticks out of the container by 40px. With box-sizing: border-box it's exactly 300px.

100% wide with padding
CSS
.parent { width: 300px; background: #f1f5f9; margin-bottom: 8px; }
.child { width: 100%; padding: 0 20px; height: 30px; background: rgb(191 219 254 / 0.8); }
.fixed .child { box-sizing: border-box; }
HTML
<div class="parent"><div class="child">content-box</div></div>
<div class="parent fixed"><div class="child">border-box</div></div>
Resultlive, in your browser
What’s happening
  1. width: 100% resolves against the container's content width: 300px of content for the child.
  2. Under content-box, the 20px left and right padding is added outside that: 300 + 40 = 340px, so the child's right edge is 40px past the container's.
  3. With border-box, the 300px includes the padding and the child fits exactly. Leaving width as auto would also fit, because auto fills the space after padding and margins (see Width, height and intrinsic sizing).

#Margin collapsing

Added

Margin collapsing is when the vertical margins of block boxes touch and the browser merges them into one margin instead of adding them. The result is the larger of the two (for positive margins), not the sum. It only happens to block-direction margins (top and bottom in horizontal writing) of boxes in normal block layout.

It exists for typography: a paragraph with margin: 1em 0 followed by another one should be 1em apart, not 2em, and a heading's bottom margin and a paragraph's top margin shouldn't stack. Treat margins as personal-space bubbles, not walls: two people who each want 30px and 20px of space stand 30px apart, because the bigger bubble already covers the smaller one.

Collapsing happens in three situations: adjacent siblings, a parent and its first or last child when nothing separates their margins (no border, padding, inline content or new formatting context), and an empty block whose own top and bottom margins touch.

Two siblings, two margins, one gap
CSS
.stack { display: flow-root; width: 260px; background: #f1f5f9; }
.a, .b { height: 50px; }
.a { margin-bottom: 30px; background: #fde68a; }
.b { margin-top: 20px; background: #bfdbfe; }
HTML
<div class="stack">
  <div class="a">margin-bottom: 30px</div>
  <div class="b">margin-top: 20px</div>
</div>
Resultlive, in your browser
What’s happening
  1. .a asks for 30px below it and .b asks for 20px above it. Added up that would be 50px.
  2. The measured gap is 30px: the two adjoining margins collapsed into one margin of max(30, 20).
  3. The stack is 130px tall: 50 + 30 + 50. The 20px margin didn't vanish from the CSS; it was simply absorbed by the larger one.
  4. This is why spacing systems often use margins in one direction only (for example only margin-bottom, or gap in flex and grid): you never have to reason about which margin wins.
AdvancedParent and child margins collapse too

The case that causes real bugs: a first child's margin-top collapses with its parent's margin-top when nothing separates them. The child's margin then appears outside the parent, pushing the whole parent down, and the parent's background doesn't cover it. Anything that puts something between the two margins stops it: padding, a border, or making the parent a new formatting context (display: flow-root, overflow other than visible/clip, flex or grid).

The margin escapes, then four ways to stop it
CSS
.case { display: flow-root; width: 260px; margin-bottom: 8px; background: #f1f5f9; }
.parent { background: #dcfce7; }
.child { margin-top: 30px; height: 30px; background: #22c55e; }
.pad  { padding-top: 1px; }
.line { border-top: 1px solid #16a34a; }
.root { display: flow-root; }
.flex { display: flex; flex-direction: column; }
HTML
<div class="case"><div class="parent" id="p0"><div class="child">collapsed</div></div></div>
<div class="case"><div class="parent pad" id="p1"><div class="child">padding</div></div></div>
<div class="case"><div class="parent line" id="p2"><div class="child">border</div></div></div>
<div class="case"><div class="parent root" id="p3"><div class="child">flow-root</div></div></div>
<div class="case"><div class="parent flex" id="p4"><div class="child">flex</div></div></div>
Resultlive, in your browser
What’s happening
  1. In the first case the green parent has no padding or border, so the child's margin-top: 30px and the parent's own top margin (0) adjoin and collapse into one 30px margin on the parent. The test measures the parent's top 30px below its grey wrapper, and the child flush with the parent's top (0).
  2. With padding-top: 1px, the padding separates the two margins. The parent stays at the top (0) and the child's margin is inside it: the child starts 31px down (1px padding plus 30px margin).
  3. A 1px top border separates them the same way: 0 and 31.
  4. display: flow-root makes the parent a new block formatting context, and margins never collapse across a formatting-context boundary: 0 and 30, without adding any pixels.
  5. A flex container lays out its children with flex layout, where margins never collapse: 0 and 30.
  6. The classic symptom is "there's a gap above my header and its background doesn't fill it": an h1's top margin escaped through the header. display: flow-root on the header is the cleanest fix.
AdvancedNegative margins and empty boxes

When negative margins are involved, the collapsed margin is the largest positive margin plus the most negative margin. Two negatives give the more negative one. And a box with no height, padding, border or content lets its own top and bottom margins touch, so they collapse with each other and with the neighbours' margins: all of them become one.

Mixed signs and an empty block
CSS
.stack { display: flow-root; width: 260px; margin-bottom: 12px; background: #f1f5f9; }
.stack > div:not(.empty) { height: 40px; }
.top { background: #fde68a; }
.bottom { background: rgb(191 219 254 / 0.85); }
#s1 .top { margin-bottom: 30px; }  #s1 .bottom { margin-top: -10px; }
#s2 .top { margin-bottom: -10px; } #s2 .bottom { margin-top: -20px; }
#s3 .top { margin-bottom: 10px; }  #s3 .bottom { margin-top: 10px; }
.empty { margin: 20px 0 30px; }
HTML
<div class="stack" id="s1"><div class="top">+30</div><div class="bottom">-10</div></div>
<div class="stack" id="s2"><div class="top">-10</div><div class="bottom">-20</div></div>
<div class="stack" id="s3"><div class="top">+10</div><div class="empty"></div><div class="bottom">+10</div></div>
Resultlive, in your browser
What’s happening
  1. +30 and -10: the largest positive (30) plus the most negative (-10) gives 20px, which is the measured gap.
  2. -10 and -20: no positives, so the most negative wins: -20px. The second box is pulled up and overlaps the first by 20px.
  3. In the third stack, the empty .empty div has no height, padding or border, so its top (20px) and bottom (30px) margins touch each other and the neighbours' 10px margins. All four collapse into one: max(10, 20, 30, 10) = 30px.
  4. Empty paragraphs and wrapper divs with nothing inside therefore don't add space the way people expect; give the element some height or padding if you want it to.
AdvancedWhere margins never collapse

Collapsing is a feature of block layout in normal flow, and only in the block direction. Margins never collapse between flex items or grid items, on floats, on absolutely positioned boxes, on inline-block boxes, or horizontally.

The same margins in flex and inline layout
CSS
.col { display: flex; flex-direction: column; width: 260px; background: #f1f5f9; margin-bottom: 12px; }
.col div { height: 40px; }
.col .a { margin-bottom: 30px; background: #fde68a; }
.col .b { margin-top: 20px; background: #bfdbfe; }
.inline span { display: inline-block; width: 60px; height: 30px; background: #fbcfe8; }
.inline .l { margin-right: 30px; }
.inline .r { margin-left: 20px; }
HTML
<div class="col"><div class="a">flex item</div><div class="b">flex item</div></div>
<div class="inline"><span class="l"></span><span class="r"></span></div>
Resultlive, in your browser
What’s happening
  1. The same 30px and 20px margins as the first demo, but the items are in a flex column: the gap is 50px. Flex layout never collapses margins.
  2. Two inline-blocks side by side (no whitespace between them) with margin-right: 30px and margin-left: 20px are 50px apart: horizontal margins always add.
  3. So if a layout's spacing suddenly doubles when you switch a container to display: flex, collapsing is why: the margins used to merge and now they don't. Using gap instead of margins avoids the difference.
Predict the gap
CSS
.first  { margin-bottom: 40px; height: 40px; }
.spacer { margin: 10px 0 50px; }            /* empty div */
.second { margin-top: -15px; height: 40px; }

Three siblings in normal flow: .first, an empty .spacer, .second. How far is .second's top from .first's bottom?

Show answer

35px.

The gap measured
CSS
.wrap { display: flow-root; width: 200px; background: #f1f5f9; }
.first  { margin-bottom: 40px; height: 40px; background: #fde68a; }
.spacer { margin: 10px 0 50px; }
.second { margin-top: -15px; height: 40px; background: #bfdbfe; }
HTML
<div class="wrap"><div class="first"></div><div class="spacer"></div><div class="second"></div></div>
Resultlive, in your browser
What’s happening
  1. The spacer is empty, so its own margins (10 and 50) touch each other, and they also touch .first's bottom margin (40) and .second's top margin (-15). All four collapse into one margin.
  2. The largest positive margin is 50 and the most negative is -15.
  3. 50 + (-15) = 35px, which the test measures.

#display: block, inline, inline-block and none

Added

display answers two questions about a box. Its outer display type says how the box takes part in its parent's layout: block (starts on a new line and, with width: auto, fills the available width) or inline (flows along a line of text with its neighbours). Its inner display type says how the box lays out its own children: normal flow, flex, grid, and so on. display: none removes the box completely.

The four classic values: block boxes stack vertically and respect width, height and all margins. Inline boxes sit in the line and size themselves to their content: width and height are ignored, and vertical padding doesn't push lines apart. Inline-block is a hybrid: it sits in the line like a word, but inside it's a block, so width, height, padding and margins all work. None generates no box at all.

Mental model: block boxes are paragraphs on a page, inline boxes are words inside a paragraph, and an inline-block is a picture placed in a sentence: it travels with the words but has its own fixed size.

Block, inline and inline-block side by side
CSS
.lane { width: 300px; background: #f1f5f9; }
.lane > * { background: #dbeafe; outline: 1px solid #2563eb; }
.inl { width: 100px; height: 60px; }       /* ignored: it's inline */
.ib  { display: inline-block; width: 80px; height: 40px; background: #fde68a; }
HTML
<div class="lane">
  <div id="b1">block</div>
  <span class="inl" id="i1">inline</span>
  <span class="inl" id="i2">inline</span>
  <span class="ib">i-b</span>
  <div id="b2">block</div>
</div>
Resultlive, in your browser
What’s happening
  1. The first <div> is block-level with width: auto, so it fills the lane: 300px wide, on its own line.
  2. The two <span>s are inline. width: 100px and height: 60px have no effect on them: each is only as wide as the word (under 60px) and as tall as the text. Oddly, getComputedStyle still reports width: 100px; it shows the property's value, not the size the box got.
  3. The .ib span is inline-block: it's placed on the same line, right after the second inline span, yet it's exactly 80 × 40 because inside it's a block box.
  4. The last <div> is block-level again, so it starts on a new line below everything.
  5. If you need a link or a badge in a sentence to have a fixed size or real vertical padding, inline-block (or inline-flex) is the tool. If you need a list of items to sit in a row, flexbox is usually better.
AdvancedGotcha: the whitespace gap between inline-blocks
A newline between tags becomes a gap
CSS
.chips { width: 300px; margin-bottom: 8px; }
.chip { display: inline-block; width: 60px; height: 30px; background: #fde68a; }
.chip + .chip { background: #fca5a5; }
HTML
<div class="chips" id="spaced">
  <span class="chip"></span>
  <span class="chip"></span>
</div>
<div class="chips" id="tight"><span class="chip"></span><span class="chip"></span></div>
Resultlive, in your browser
What’s happening
  1. In #spaced the two chips are on separate lines of HTML. The newline and indentation collapse into one space, and that space is rendered between the chips: the test measures a gap of a few pixels (the width of a space in the frame's 15px font).
  2. In #tight the tags touch, so there's no text between them and the gap is 0.
  3. Fixes, in order of preference: use display: flex with gap on the parent (whitespace between flex items is ignored), or remove the whitespace in the markup. The old hack font-size: 0 on the parent also works but breaks em sizes inside.
AdvancedGotcha: the gap under an inline image

Images and inline SVGs are inline by default, so they sit on the text baseline, and the line box keeps room below the baseline for letters with descenders (g, p, y). The result is a few pixels of empty space under every image in a container.

An inline image sits on the baseline
CSS
.pic { width: 120px; margin-bottom: 8px; background: #fce7f3; }
.block svg { display: block; }
.top svg { vertical-align: top; }
HTML
<div class="pic" id="p1"><svg width="120" height="60"><rect width="120" height="60" fill="#db2777"/></svg></div>
<div class="pic block" id="p2"><svg width="120" height="60"><rect width="120" height="60" fill="#db2777"/></svg></div>
<div class="pic top" id="p3"><svg width="120" height="60"><rect width="120" height="60" fill="#db2777"/></svg></div>
Resultlive, in your browser
What’s happening
  1. In #p1 the SVG is inline, so its bottom edge sits on the baseline of the line it's in. The line also reserves space below the baseline for descenders, so the container is taller than 60px and the pink background shows under the image.
  2. display: block takes the image out of the line altogether: the container is exactly 60px.
  3. vertical-align: top (or middle, bottom) keeps it inline but stops it sitting on the baseline, which also removes the gap: 60px.
  4. That's why most resets include img, svg, video { display: block; }.
AdvancedDefault display values of common elements

The browser's built-in stylesheet gives each element its default display. Knowing a few by heart helps when debugging:

Default display values
HTML
<div id="d">div</div><p id="p">p</p><span id="s">span</span><a id="a" href="#">a</a>
<ul><li id="li">li</li></ul>
<button id="btn">button</button><input id="inp"><img id="img" alt="">
<table id="tbl"><tr id="tr"><td id="td">td</td></tr></table>
Resultlive, in your browser
What’s happening
  1. div and p are block; span, a and img are inline. (img is a replaced inline element, which is why it can have a width and height even though it's inline.)
  2. li is list-item: a block box that also generates a ::marker (the bullet).
  3. button and input are inline-block in Chrome 155: they sit in text but have their own size.
  4. Table elements have their own values (table, table-row, table-cell), which is why setting display: block on a <td> breaks the table layout.
AdvancedThe two-keyword display syntax

CSS Display Level 3 lets you write the two types separately: display: block flow is block, display: inline flex is inline-flex, display: block flex is flex. The one-word values are shorthands for these pairs. Support: Chrome 115, Firefox 70 and Safari 15 (caniuse, October 2026), so it works in current browsers, but the one-word forms are what you'll see in most code.

The pairs and their one-word names
CSS
.a { display: inline flex; }
.b { display: block flow; }
.c { display: block flex; }
.d { display: block flow-root; }
HTML
<div class="a"></div><div class="b"></div><div class="c"></div><div class="d"></div>
Resultlive, in your browser
What’s happening
  1. Chrome serialises each pair back to its one-word name: inline flex reads back as inline-flex, block flow as block, block flex as flex.
  2. The point of the syntax is the mental model: every box has an outside (how it sits among siblings) and an inside (how it arranges children). inline-block is really inline flow-root: inline outside, a new block formatting context inside.
Advanceddisplay: contents removes the box, keeps the children

display: contents makes the element generate no box of its own while its children still render, as if they were the parent's children. It's handy when a wrapper element (needed for markup reasons) gets in the way of a flex or grid layout.

A wrapper that disappears from the layout
CSS
.row { display: flex; gap: 10px; }
.row span { width: 60px; height: 30px; background: #c7d2fe; }
.group { display: contents; }
HTML
<div class="row">
  <span id="one"></span>
  <div class="group"><span id="two"></span><span id="three"></span></div>
</div>
Resultlive, in your browser
What’s happening
  1. .group has no box: its rectangle is 0 × 0.
  2. Its two spans are laid out as if they were direct children of .row: they're flex items on the same line as #one, separated by the row's 10px gap.
  3. Without display: contents, .group would be one flex item containing two spans, and the gap wouldn't apply between them.
Advanceddisplay: contents and accessibility
Width on a span
HTML
<span style="width: 200px; height: 50px; padding: 10px; background: gold">Hi</span>

How wide is the gold background?

Show answer

As wide as the word "Hi" plus 20px of padding. width and height don't apply to non-replaced inline boxes.

The span measured
CSS
.ref { background: #e5e7eb; }
HTML
<span id="q" style="width: 200px; height: 50px; padding: 10px; background: gold">Hi</span>
<br><br>
<span id="ref" class="ref">Hi</span>
Resultlive, in your browser
What’s happening
  1. A <span> is inline by default, and an inline, non-replaced box takes its width from its content. width: 200px and height: 50px are ignored for layout.
  2. The test compares it with an unstyled "Hi": the gold span is exactly 20px wider, the left and right padding.
  3. The vertical padding is painted too, but it overlaps neighbouring lines instead of making room (see The box model).
  4. Add display: inline-block and the box becomes 220 × 70 (content 200 × 50 plus padding).

#Hiding things: display none vs visibility hidden vs opacity 0

Added

There are three common ways to make something invisible, and they hide different amounts of the element. display: none removes the box entirely: no space, no clicks, no focus, nothing for screen readers. visibility: hidden keeps the box and its space but makes it invisible and non-interactive, and it's also hidden from assistive technology. opacity: 0 only makes it transparent: it still takes space, still receives clicks, can still be focused, and is still read by screen readers.

Mental model: display: none is taking the actor off the stage. visibility: hidden is the actor standing still in the dark: their spot is taken but nobody can see or bump into them. opacity: 0 is an invisible actor who is still fully there: you can walk into them and they still speak their lines.

Takes spaceReceives clicksFocusableIn the accessibility tree
display: nonenononono
visibility: hiddenyesnonono
opacity: 0yesyesyesyes
Three ways to hide a button
CSS
.shelf { display: flex; gap: 8px; width: 300px; }
.shelf button { width: 60px; height: 40px; margin: 0; }
.none   { display: none; }
.hidden { visibility: hidden; }
.clear  { opacity: 0; }
HTML
<div class="shelf">
  <button id="a">A</button>
  <button id="b" class="none">B</button>
  <button id="c" class="hidden">C</button>
  <button id="d" class="clear">D</button>
  <button id="e">E</button>
</div>
Resultlive, in your browser
What’s happening
  1. Space. B (display: none) has a 0 × 0 rectangle and takes no slot: E sits 204px after A, which is three slots of 68px (a 60px button plus the 8px gap: A, C, D). C and D are both still 60px wide.
  2. Clicks. document.elementFromPoint() at the centre of C's spot returns the .shelf behind it: a visibility: hidden element doesn't receive pointer events. At D's centre it returns D itself, so a user clicking that empty-looking spot would press the invisible button.
  3. Focus. Calling .focus() fails on B and C and succeeds on D. A keyboard user tabbing through the page would land on an invisible opacity: 0 button.
  4. Accessibility. Chrome's computed accessible name is empty for B and C because both are out of the accessibility tree, and "D" for D, which a screen reader still announces.
  5. element.checkVisibility() gives the same answers in code: false for B, false for C when you pass visibilityProperty: true, and false for D only when you pass opacityProperty: true. By default it counts an opacity: 0 element as visible.
  6. The rule of thumb: hide it from everyone with display: none (or the hidden attribute), keep its space with visibility: hidden, and use opacity: 0 only as a step in an animation, never as the final hidden state.
AdvancedChildren can undo visibility, not opacity

visibility is inherited, and a child can set visibility: visible to show itself inside a hidden parent. opacity multiplies down the tree: a child can't be more opaque than its parent, so opacity: 1 on the child doesn't help. And nothing inside a display: none element can render.

A child tries to show itself
CSS
.parent { width: 200px; height: 60px; margin-bottom: 8px; background: #e0e7ff; }
.peek { display: block; width: 80px; height: 30px; background: #6366f1; }
.p-hidden { visibility: hidden; }
.p-hidden .peek { visibility: visible; }
.p-clear { opacity: 0; }
.p-clear .peek { opacity: 1; }
HTML
<div class="parent p-hidden"><span class="peek" id="v"></span></div>
<div class="parent p-clear"><span class="peek" id="o"></span></div>
Resultlive, in your browser
What’s happening
  1. The first parent is visibility: hidden, but its child sets visibility: visible. The child is painted and clickable: elementFromPoint on it returns the child, and checkVisibility says it's visible.
  2. The second parent is opacity: 0. The child's own opacity is 1, but opacity applies to the parent's whole rendered group, so the child is still fully transparent: checkVisibility({ opacityProperty: true }) says false.
  3. This is how "show only this part of a hidden panel" tricks work with visibility, and why they can't work with opacity.
AdvancedWhich of the three can animate

opacity interpolates smoothly, so it's the one that fades. visibility is animatable too, but discretely: during a transition between visible and hidden it stays visible the whole time and flips only at the end. That is why the classic fade-out pattern transitions both: the opacity fades while visibility keeps the element in place, then visibility switches it off so it can't be clicked. display doesn't transition by default; doing that needs transition-behavior: allow-discrete (see Entry animations with @starting-style and transition-behavior).

Fading out with opacity and visibility
CSS
.toast { width: 200px; padding: 10px; background: #fef3c7; transition: opacity 1s, visibility 1s; }
.toast.out { opacity: 0; visibility: hidden; }
.gone { transition: display 1s; }
.gone.out { display: none; }
HTML
<div class="toast" id="t">Saved</div>
<div class="gone" id="g">No transition</div>
Resultlive, in your browser
What’s happening
  1. Adding .out to the toast starts two CSS transitions, one for opacity and one for visibility; the test lists them with getAnimations().
  2. Adding .out to #g changes display to none with transition: display 1s, but no transition starts: the box disappears at once.
  3. The test pauses the toast's transitions at 900ms of 1000ms. Opacity is almost 0, yet visibility is still visible: the discrete flip waits for the end.
  4. After finishing them, visibility is hidden, so the invisible toast no longer takes clicks or focus. The reader can toggle a class in DevTools on the live result to watch the fade.
AdvancedThe hidden attribute loses to display rules
A hidden toolbar that still shows
CSS
.toolbar { display: flex; gap: 8px; height: 40px; margin-bottom: 8px; background: #fde68a; }
/* the usual fix (normally written as just [hidden]); scoped to .fixed here so both cases show */
.fixed [hidden] { display: none !important; }
HTML
<div class="toolbar" id="t1" hidden>I have the hidden attribute</div>
<div class="fixed"><div class="toolbar" id="t2" hidden>So do I</div></div>
Resultlive, in your browser
What’s happening
  1. #t1 has hidden, but .toolbar { display: flex } is an author rule, which beats the browser's [hidden] { display: none }. Its computed display is flex and it's 40px tall on screen.
  2. The fix adds !important to a [hidden] rule of your own, which no ordinary display declaration can beat. #t2 computes to display: none.
  3. Many resets include [hidden] { display: none !important; } for exactly this reason.
AdvancedVisually hidden: invisible but still announced

Sometimes you want the opposite of visibility: hidden: text that screen readers announce but nobody sees, such as the label of an icon-only button. None of the three properties does that (opacity: 0 still takes space and catches clicks). The standard "visually hidden" class shrinks the element to one clipped pixel instead. More in Hiding content.

An icon button with a hidden label
CSS
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.icon { width: 40px; height: 40px; padding: 0; }
HTML
<button class="icon" id="close">
  <svg width="16" height="16" aria-hidden="true"><path d="M2 2l12 12M14 2L2 14" stroke="currentColor" stroke-width="2"/></svg>
  <span class="visually-hidden">Close dialog</span>
</button>
Resultlive, in your browser
What’s happening
  1. The SVG has aria-hidden="true", so it contributes nothing to the accessible name.
  2. The span's text is still in the accessibility tree, so Chrome computes the button's name as "Close dialog", which is what a screen reader announces.
  3. Visually, the span is a 1 × 1 absolutely positioned box clipped to nothing, so it neither shows nor affects the 40px button's layout.
Advancedcontent-visibility: hidden skips rendering

content-visibility: hidden hides an element's contents (the element's own box stays) and skips rendering them, like display: none does for find-in-page and focus, but the browser can keep the already-computed rendering state so showing it again is cheap. Pair it with contain-intrinsic-size so the element keeps a size while its contents are skipped. MDN marks content-visibility as Baseline 2024 (newly available since September 2024).

The box stays, its contents are skipped
CSS
.panel { content-visibility: hidden; contain-intrinsic-size: auto 60px; width: 200px; background: #e0f2fe; }
HTML
<div class="panel"><p id="inner">Expensive content</p></div>
Resultlive, in your browser
What’s happening
  1. .panel is 60px tall: its contents are skipped, so contain-intrinsic-size: auto 60px supplies the height.
  2. checkVisibility() reports the paragraph inside as not visible, because it's in a skipped subtree.
  3. It's a performance tool for big tabbed or virtualised UIs more than a general way to hide things.
Which one can still be clicked?

A modal overlay is "closed" with one of display: none, visibility: hidden or opacity: 0, and users report that clicking the page sometimes does nothing. Which one was used, and why?

Show answer

opacity: 0. The overlay is transparent but still there, covering the page and catching every click.

What’s happening
  1. display: none removes the overlay's box, so clicks reach the page.
  2. visibility: hidden keeps the box but it doesn't receive pointer events, so clicks pass through to the page (the first demo's elementFromPoint on C returned the element behind it).
  3. opacity: 0 keeps the box and its hit-testing: elementFromPoint on D returned D. An invisible full-screen overlay therefore swallows clicks, and its buttons can still be tabbed to.
  4. If you fade it out with opacity, finish with visibility: hidden (as in the fade demo) or add pointer-events: none, and for keyboard users make sure nothing inside can be focused.

#Width, height and intrinsic sizing

Added

There are two ways a box can get its size. Extrinsic sizing comes from outside: the container decides (width: auto on a block fills the available width, width: 50% takes half of the containing block, width: 300px is fixed). Intrinsic sizing comes from the content: how wide does the box need to be for what's inside?

CSS gives the intrinsic sizes keywords. min-content is the narrowest the box can be without overflowing: every possible line break taken, so it's the width of the longest unbreakable piece (the longest word, the widest image). max-content is the width the content wants with no line breaks at all. fit-content is "shrink-to-fit": use max-content if there's room, otherwise fill the available space, but never go below min-content. As a formula: min(max-content, max(min-content, available)).

Mental model: imagine the content as a paragraph of words on a roller blind. min-content is the blind pulled as narrow as it goes (one word per line), max-content is the blind fully open (everything on one line), and fit-content opens it as far as the window allows. height: auto on a block, by contrast, is always intrinsic: as tall as its content.

One set of content under each sizing keyword
CSS
.shelf { width: 300px; background: #f1f5f9; }
.shelf > div { margin-bottom: 6px; background: #dbeafe; outline: 1px solid #2563eb; }
.shelf b { display: inline-block; width: 60px; height: 24px; background: #3b82f6; outline: 1px solid #fff; }
.half { width: 50%; }
.min  { width: min-content; }
.max  { width: max-content; }
.fit  { width: fit-content; }
HTML
<div class="shelf">
  <div class="auto"><b></b><b></b><b></b></div>
  <div class="half"><b></b><b></b><b></b></div>
  <div class="min"><b></b><b></b><b></b></div>
  <div class="max"><b></b><b></b><b></b></div>
  <div class="fit"><b></b><b></b><b></b></div>
</div>
Resultlive, in your browser
What’s happening
  1. Each row holds three 60px blocks with no whitespace between them. They behave like 60px "words": a line can break between any two of them, but not inside one.
  2. width: auto is extrinsic: a block fills its container, 300px, and the three blocks fit on one line. width: 50% is half the container, 150px, so only two fit per line and the content wraps onto 2 rows.
  3. min-content takes every break opportunity: the box is as wide as the widest unbreakable piece, 60px, with one block per row (3 rows).
  4. max-content takes none: 180px, everything on 1 row.
  5. fit-content is min(180, max(60, 300)) = 180px: the content's preferred width, because there's room for it. It's the same "shrink-wrap" sizing that floats, inline-blocks and absolutely positioned boxes get automatically.
Advancedfit-content in a narrow container

fit-content only differs from max-content when the container is too narrow: it then takes the available width, down to the min-content floor.

fit-content clamps between min and max
CSS
.box { margin-bottom: 8px; background: #f1f5f9; }
.box > div { width: fit-content; background: #dcfce7; outline: 1px solid #16a34a; }
.box b { display: inline-block; width: 60px; height: 20px; background: #22c55e; }
HTML
<div class="box" style="width: 300px"><div id="f300"><b></b><b></b><b></b></div></div>
<div class="box" style="width: 100px"><div id="f100"><b></b><b></b><b></b></div></div>
<div class="box" style="width: 40px"><div id="f40"><b></b><b></b><b></b></div></div>
Resultlive, in your browser
What’s happening
  1. In the 300px container: min(180, max(60, 300)) = 180.
  2. In the 100px container: min(180, max(60, 100)) = 100. The box fills the space and its content wraps.
  3. In the 40px container: min(180, max(60, 40)) = 60. The box refuses to be narrower than its widest block, so it overflows the container by 20px.
Advancedmin-width and max-width win over width

min-width and max-width clamp width, and when they conflict with each other, min-width wins. The order of precedence is min-width > max-width > width. The same applies to the height trio.

Conflicting size constraints
CSS
.a { width: 380px; max-width: 200px; height: 20px; background: #fde68a; margin-bottom: 6px; }
.b { width: 380px; max-width: 200px; min-width: 300px; height: 20px; background: #fecaca; margin-bottom: 6px; }
.c { width: 100%; max-width: 250px; height: 20px; background: #bfdbfe; }
HTML
<div style="width: 320px">
  <div class="a"></div><div class="b"></div><div class="c"></div>
</div>
Resultlive, in your browser
What’s happening
  1. .a asks for 380px but max-width: 200px caps it: 200.
  2. .b also has min-width: 300px, which conflicts with the 200px maximum. The spec resolves it in min-width's favour: 300.
  3. .c is the everyday pattern: width: 100%; max-width: 250px fills small containers and stops growing at 250px. (max-width alone, with width: auto, does the same for blocks.)
AdvancedPercentage heights need a definite parent height

A percentage width always has something to resolve against, because a container's width is known before its children are laid out. A percentage height resolves against the containing block's height, and if that height depends on the content (height: auto), the percentage can't be resolved and behaves as auto.

The same 50% height in two parents
CSS
.parent { width: 200px; margin-bottom: 8px; background: #f1f5f9; }
.sized { height: 200px; }
.child { height: 50%; background: #c7d2fe; }
.child i { display: block; height: 20px; }
HTML
<div class="parent" id="auto"><div class="child"><i></i></div></div>
<div class="parent sized" id="sized"><div class="child"><i></i></div></div>
Resultlive, in your browser
What’s happening
  1. In #auto the parent's height is auto: it depends on the child, and the child would depend on the parent. CSS breaks the cycle by treating the child's 50% as auto, so the child is just its content, 20px.
  2. In #sized the parent is 200px tall, a definite height, so 50% resolves to 100px.
  3. That's why full-height layouts used to need html, body { height: 100% } all the way down. Today flexbox or grid (which stretch children), or min-height: 100dvh (see Viewport units on phones), are simpler.
Advancedwidth: auto vs width: 100%

For a block, width: auto means "fill the space after margins, borders and padding", while width: 100% means "make the content box as wide as the container", with margins and padding added on top. They look identical until you add a margin.

The same margin with auto and 100%
CSS
.parent { width: 300px; margin-bottom: 8px; background: #f1f5f9; }
.child { margin: 0 20px; height: 30px; background: rgb(253 186 116 / 0.85); }
.full { width: 100%; }
HTML
<div class="parent" id="pa"><div class="child">auto</div></div>
<div class="parent" id="pf"><div class="child full">100%</div></div>
Resultlive, in your browser
What’s happening
  1. With width: auto, the child takes 300 - 20 - 20 = 260px, sitting neatly inside its margins.
  2. With width: 100%, the child's width is the parent's full 300px. The left margin then pushes it 20px to the right, so it sticks out 20px past the parent.
  3. For blocks, leave width as auto unless you have a reason. The min-width: auto rule that makes flex items refuse to shrink is a related intrinsic-sizing surprise: see Why flex items overflow.
Advancedmin-content for text is the longest word

With text, the unbreakable pieces are words, so min-content is the width of the longest word.

A paragraph at min-content
CSS
.p { width: min-content; margin: 0; background: #fef9c3; font-size: 16px; }
HTML
<p class="p" id="p">A remarkably long word</p>
Resultlive, in your browser
What’s happening
  1. The paragraph's width is set to min-content, so the browser takes every line-break opportunity, which for text means every space.
  2. The test measures the word "remarkably" with a Range and finds the paragraph exactly that wide; every other word fits on a line of that width, and the text wraps one word per line.
  3. This is the width a table column or a grid track with min-content would collapse to, and the floor below which fit-content won't go.
What width does fit-content give?
CSS
.parent { width: 250px; }
.child  { width: fit-content; }

The child holds four 80px inline-blocks with no whitespace between them. How wide is it, and how many rows does it use?

Show answer

250px wide, on 2 rows (three blocks fit on the first row, one on the second). The 250px is the container's width, not a multiple of 80.

The child measured
CSS
.parent { width: 250px; background: #f1f5f9; }
.child { width: fit-content; background: #dcfce7; }
.child b { display: inline-block; width: 80px; height: 20px; background: #22c55e; outline: 1px solid #fff; }
HTML
<div class="parent"><div class="child"><b></b><b></b><b></b><b></b></div></div>
Resultlive, in your browser
What’s happening
  1. min-content is 80px (one block) and max-content is 320px (four in a row). The available width is 250px.
  2. fit-content = min(320, max(80, 250)) = 250px.
  3. Inside 250px, three blocks (240px) fit on the first line and the fourth wraps. The box doesn't shrink to the 240px its widest line actually uses: shrink-to-fit takes the available width when the content doesn't fit, which is why a wrapped "fit-content" box often has empty space at its end.

#Overflow and scroll containers

Added

Overflow is what happens when content is bigger than its box: a fixed-height card with too much text, a long URL in a narrow column. The overflow property decides what to do with the part that doesn't fit, and the key point is that overflowing content never changes the box's size or pushes anything else: layout uses the box's own size, whatever spills out.

The values: visible (the default: content spills out and is painted over whatever is next), hidden (clipped at the padding box, and the box becomes a scroll container without scrollbars: the user can't scroll it, but code can), clip (clipped too, but not a scroll container, so it can't be scrolled at all, even by code), auto (a scroll container that shows scrollbars only when needed), and scroll (a scroll container that always reserves scrollbars on systems with classic scrollbars).

Mental model: the box is a window. visible means the content is painted on the glass and can hang outside the frame. hidden is a window with a blind you can only move from behind the wall (by code). clip is a window bricked up at the edges: what's outside is gone. auto and scroll give the user a handle to move the view.

The same overflowing content under four values
CSS
.cases { display: flex; gap: 16px; align-items: flex-start; }
.win { width: 80px; height: 80px; border: 2px solid #475569; background: #f8fafc; }
.tall { height: 200px; background: linear-gradient(#bfdbfe, #1d4ed8); }
.visible { overflow: visible; }
.hidden  { overflow: hidden; }
.clip    { overflow: clip; }
.auto    { overflow: auto; }
.after { margin-top: 8px; height: 20px; background: #fde68a; }
HTML
<div class="cases">
  <div class="win visible"><div class="tall">visible</div></div>
  <div class="win hidden"><div class="tall">hidden</div></div>
  <div class="win clip"><div class="tall">clip</div></div>
  <div class="win auto"><div class="tall">auto</div></div>
</div>
<div class="after">next element</div>
Resultlive, in your browser
What’s happening
  1. Each window is 80px tall plus a 2px border, and the content inside is 200px tall. The flex row is 84px tall and the yellow element below sits exactly its 8px margin after it: the overflowing content didn't push anything.
  2. With visible, the gradient keeps going below the window's bottom border and spills across the yellow element below it (look at the live result). The yellow element didn't move; the two simply overlap.
  3. With hidden, the content is clipped at the padding box. The box is still a scroll container: scrollHeight is 200 (all the content) and clientHeight is 80 (the visible part), and setting scrollTop = 50 scrolls it to 50.
  4. With clip, the content is clipped the same way but the box isn't a scroll container: scrollTop = 50 stays 0.
  5. With auto, the box scrolls for the user too (try it in the live result; scrollbars appear if your system shows them), and scrollTop works.
  6. Choose hidden when code might need to scroll the box (a carousel), clip when nothing should ever scroll it, and auto for user-scrollable panels. MDN notes that with hidden a focused element inside gets scrolled into view, while with clip it doesn't, which can leave keyboard focus on something you can't see.
AdvancedGotcha: one axis hidden makes the other scroll
What overflow-y computes to
CSS
.a { overflow-x: hidden; }
.b { overflow-x: clip; }
.c { overflow: visible hidden; }
HTML
<div class="a"></div><div class="b"></div><div class="c"></div>
Resultlive, in your browser
What’s happening
  1. .a only sets overflow-x: hidden, yet its overflow-y computes to auto: the element is now a scroll container in both directions.
  2. .b uses overflow-x: clip, and overflow-y stays visible, because clip and visible can be mixed.
  3. .c sets visible for x and hidden for y with the two-value shorthand, and the visible axis computes to auto.
  4. overflow: clip is supported in Chrome 90+, Firefox 81+ and Safari 16+ (caniuse, October 2026).
AdvancedTruncating text with an ellipsis

text-overflow: ellipsis replaces the cut-off end of a line with "…", but it only kicks in when three things are true: the text is kept on one line (white-space: nowrap), the box clips it (overflow: hidden or clip), and the box has a constrained width. Forgetting overflow: hidden is the usual reason it "doesn't work".

A truncated title
CSS
.title {
  width: 180px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  background: #f1f5f9;
}
HTML
<div class="title" id="t">A very long article title that will not fit</div>
Resultlive, in your browser
What’s happening
  1. The title is kept on one line, so its text is wider than the 180px box: scrollWidth is larger than clientWidth.
  2. overflow: hidden clips the rest, and text-overflow: ellipsis draws "…" at the clip edge (visible in the live result; a DOM test can't read painted glyphs).
  3. The full text is still in the DOM, so screen readers and copy-paste get the whole title. Show it in a tooltip or on a details page if users need it.
AdvancedClamping text to a number of lines

To cut text after a number of lines, the recipe that works across engines is the prefixed one: display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden. caniuse lists it in Chrome, Firefox 68+ and Safari; the unprefixed line-clamp property is still "Limited availability" on MDN (October 2026).

Three lines, then an ellipsis
CSS
.clamp {
  width: 200px;
  line-height: 20px;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  background: #f1f5f9;
}
HTML
<p class="clamp" id="c">Margins, padding and borders make up the box model, and once you know which box each property sizes, most layout bugs become arithmetic you can do in your head during an interview.</p>
Resultlive, in your browser
What’s happening
  1. line-height: 20px fixes each line's height, and -webkit-line-clamp: 3 limits the box to 3 lines: it measures exactly 60px tall.
  2. scrollHeight is larger, because the rest of the text is still there, just clipped. Chrome draws an ellipsis at the end of the third line (visible in the live result).
  3. All four declarations are needed; the clamp has no effect without display: -webkit-box and the vertical orientation.
Advancedoverflow creates a new formatting context

Any overflow other than visible or clip makes the element establish a new block formatting context. That's why overflow: hidden was the old trick to make a parent contain its floated children and to stop margins collapsing through it. Today display: flow-root does the same thing without clipping anything.

A parent containing a float
CSS
.parent { width: 260px; margin-bottom: 8px; background: #f1f5f9; outline: 1px dashed #94a3b8; }
.float { float: left; width: 80px; height: 60px; background: #fda4af; }
.bfc { overflow: hidden; }
.root { display: flow-root; }
HTML
<div class="parent" id="p1"><div class="float"></div></div>
<div class="parent bfc" id="p2"><div class="float"></div></div>
<div class="parent root" id="p3"><div class="float"></div></div>
Resultlive, in your browser
What’s happening
  1. A float is taken out of normal flow, so the plain parent has no in-flow content and is 0px tall; the float hangs out of it.
  2. overflow: hidden makes the parent a new block formatting context, which must contain its floats: 60px.
  3. display: flow-root gives the same 60px without clipping anything, and without making the element a scroll container. More in Floats and clearing.
AdvancedScrollbars aren't measured here
Which box can JavaScript scroll?

Four boxes have the same overflowing content, with overflow set to hidden, clip, auto and visible. You run box.scrollTop = 50 on each. Which of them end up scrolled?

Show answer

hidden and auto. clip and visible don't create a scroll container, so scrollTop stays 0.

scrollTop on each value
CSS
.box { width: 100px; height: 50px; } .box div { height: 150px; }
.h { overflow: hidden; } .c { overflow: clip; } .a { overflow: auto; } .v { overflow: visible; }
HTML
<div class="box h"><div></div></div><div class="box c"><div></div></div>
<div class="box a"><div></div></div><div class="box v"><div></div></div>
Resultlive, in your browser
What’s happening
  1. overflow: hidden clips the content but keeps a scroll position, so scrollTop = 50 works: the user can't scroll it, code can.
  2. overflow: auto is a normal scroll container: 50.
  3. overflow: clip clips without a scroll container, and visible doesn't clip at all: scrollTop stays 0 for both.
  4. This is the practical difference between hidden and clip, and why clip is the right choice when you only want to cut off decoration.

#CSS units: px, em, rem, %, vw, vh, ch

Added

CSS lengths come in a few families, and each answers "relative to what?" differently. Absolute: px is the CSS pixel, a reference unit (1in is defined as 96px), not a physical device pixel; on a high-density screen one CSS pixel covers several device pixels. Font-relative: em is the element's own computed font size (except inside font-size itself, where it means the parent's), rem is the root (<html>) font size, and ch is the width of the "0" character in the element's font. Viewport: vw and vh are 1% of the viewport's width and height. Percentages are relative to something that depends on the property.

The mental model: rem is a fixed ruler for the whole page, em is a ruler that changes size with each component. Use rem when you want things to scale with the user's base font setting but stay consistent across the page; use em when something should scale with its own text (a button's padding, an icon next to a label).

Interviewers mostly want three things: why em compounds when nested, why rem respects user font settings better than px, and what a percentage refers to in different properties.

Five units in one 300px card
CSS
html { font-size: 16px; }   /* the usual browser default, written out */
.card { width: 300px; font-size: 20px; background: #f8fafc; }
.card div { height: 22px; margin-bottom: 4px; background: #c7d2fe; font-size: 12px; }
.card .em  { width: 10em; }
.card .rem { width: 10rem; }
.card .pct { width: 50%; }
.card .vw  { width: 25vw; }
.card .px  { width: 1in; }
HTML
<div class="card">
  <div class="em">10em</div>
  <div class="rem">10rem</div>
  <div class="pct">50%</div>
  <div class="vw">25vw</div>
  <div class="px">1in</div>
</div>
Resultlive, in your browser
What’s happening
  1. 10em on a bar whose own font-size is 12px is 120px. em in a property other than font-size uses the element's own computed font size, not the card's 20px.
  2. 10rem is 160px: 10 × the root's 16px, regardless of the card's or the bar's font size.
  3. 50% of the card's 300px content width is 150px.
  4. 25vw is a quarter of the viewport width: 160px in the 640px-wide test frame (on this page, a quarter of the result frame's width, so it changes as you resize the window).
  5. 1in is exactly 96px: CSS defines physical units in terms of the reference pixel, so they're not real inches on screen.
Advancedem compounds when nested, rem doesn't

In font-size, em multiplies the parent's computed font size. Nest elements that each say font-size: 1.2em and the multiplier compounds at every level. rem always multiplies the root's size, so nesting doesn't matter.

Nested lists with em and rem font sizes
CSS
.em li  { font-size: 1.2em; }
.rem li { font-size: 1.2rem; }
ul { margin: 0; }
HTML
<ul class="em"><li id="e1">one<ul><li id="e2">two<ul><li id="e3">three</li></ul></li></ul></li></ul>
<ul class="rem"><li id="r1">one<ul><li id="r2">two<ul><li id="r3">three</li></ul></li></ul></li></ul>
Resultlive, in your browser
What’s happening
  1. The frame's body text is 15px (set by the demo base stylesheet), and the root <html> is the browser default 16px.
  2. With em: the first li is 15 × 1.2 = 18px, the nested one 18 × 1.2 = 21.6px, the third 21.6 × 1.2 = 25.92px. Each level multiplies its parent's result.
  3. With rem: every level is 16 × 1.2 = 19.2px, because all of them refer to the root.
  4. This compounding is the main reason design systems size type in rem. em is still the right unit when you want something to follow its context, like padding inside a button.
Advancedem in font-size vs em everywhere else

em in font-size means the parent's font size (it has to: the element's own size is what's being computed). In every other property, em means the element's own computed font size. That makes em padding scale with the component's text.

A button sized in em
CSS
.toolbar { font-size: 15px; }
.btn { font-size: 2em; padding: 0.5em 1em; border: 0; }
HTML
<div class="toolbar"><button class="btn">Save</button></div>
Resultlive, in your browser
What’s happening
  1. font-size: 2em uses the parent's 15px: the button's text is 30px.
  2. padding: 0.5em 1em uses the button's own 30px: 15px vertically and 30px horizontally.
  3. Change only the button's font-size and its padding scales with it, which is exactly what you want for small, medium and large button variants.
AdvancedWhat each percentage refers to
Property% is relative toProven in
width, left/rightthe containing block's widththe units demo above
height, top/bottomthe containing block's height (only if it's definite)Width, height and intrinsic sizing
padding, margin (all four sides)the containing block's widthThe box model
font-sizethe parent's font sizethe demo below
line-heightthe element's own font sizethe demo below
transform: translate()the element's own border boxthe demo below
font-size, line-height and translate percentages
CSS
.parent { font-size: 20px; }
.child { font-size: 150%; line-height: 150%; }
.slide { width: 80px; height: 30px; background: #fbcfe8; transform: translateX(50%); }
.ghost { width: 80px; height: 30px; background: #e5e7eb; }
HTML
<div class="parent"><p class="child">text</p></div>
<div class="ghost"></div>
<div class="slide"></div>
Resultlive, in your browser
What’s happening
  1. font-size: 150% is 150% of the parent's 20px: 30px.
  2. line-height: 150% is 150% of the element's own 30px font size: 45px.
  3. translateX(50%) moves the 80px box by half of its own width, 40px, not half of its container. That's why translate(-50%, -50%) centres an element on a point.
Advancedline-height inherits differently with units
Unitless vs percentage line-height
CSS
.pct { font-size: 10px; line-height: 150%; }
.num { font-size: 10px; line-height: 1.5; }
.big { font-size: 30px; }
HTML
<div class="pct"><p class="big" id="a">big text</p></div>
<div class="num"><p class="big" id="b">big text</p></div>
Resultlive, in your browser
What’s happening
  1. On .pct, 150% is computed against the parent's 10px font: 15px. That length is what the child inherits, so the 30px child gets 15px lines and its text overlaps.
  2. On .num, the computed value is the number 1.5, and the child applies it to its own 30px: 45px.
  3. That's why body text styles use a unitless line-height.
AdvancedThe ch unit and readable line length

1ch is the advance width of the "0" glyph in the element's font. In a monospace font every character is that wide, so ch is exact; in proportional fonts it's an approximation. max-width: 65ch is a common way to keep paragraphs at a comfortable reading length that scales with the font.

10ch in a monospace font
CSS
.mono { font: 16px ui-monospace, Menlo, monospace; }
.bar { width: 10ch; height: 20px; background: #a7f3d0; }
HTML
<div class="mono">
  <div class="bar"></div>
  <span id="zeros">0000000000</span>
</div>
Resultlive, in your browser
What’s happening
  1. The bar is 10ch wide and the span holds ten "0" characters in the same font.
  2. The test finds them the same width (to half a pixel): ch is literally the width of a zero in that font.
  3. Change the font size and both scale together, which is the point: ch measures text, not pixels.
AdvancedViewport units, and why vh misbehaves on phones

1vw is 1% of the viewport's width and 1vh 1% of its height. In this page's demos the "viewport" is the result frame, so 10vh here is 10% of the frame's height. On phones, 100vh is famously taller than the visible area because browser toolbars appear and disappear; svh, lvh and dvh were added to fix that (see Viewport units on phones). Newer still are container query units such as cqi, relative to a container instead of the viewport (see Container queries).

vh measured in the frame
CSS
.bar { width: 10vh; height: 20px; background: #fde68a; }
HTML
<div class="bar"></div>
Resultlive, in your browser
What’s happening
  1. The bar's width is 10vh (height units can size any length).
  2. The test frame is 600px tall, so the bar is 60px wide; in the page frame, which sizes itself to its content, it will differ.
  3. vw likewise ignores the containing block: 100vw is the whole viewport width even inside a narrow column, as the 25vw bar in the first demo showed.
AdvancedWhy rem beats px for font sizes
Nested em
CSS
body    { font-size: 16px; }
.outer  { font-size: 1.25em; }
.inner  { font-size: 0.8em; padding: 1em; }

.inner is inside .outer. What are .inner's font size and padding?

Show answer

Font size 16px, padding 16px.

The values computed
CSS
.body { font-size: 16px; }
.outer { font-size: 1.25em; }
.inner { font-size: 0.8em; padding: 1em; }
HTML
<div class="body"><div class="outer"><div class="inner">inner</div></div></div>
Resultlive, in your browser
What’s happening
  1. .outer is 1.25 × 16 = 20px (the demo uses a .body div standing in for body).
  2. .inner's font-size: 0.8em multiplies its parent's 20px: 16px.
  3. padding: 1em multiplies the element's own font size, now 16px: 16px.

#aspect-ratio and object-fit

Added

aspect-ratio gives a box a preferred ratio of width to height, so you set one dimension and the browser works out the other: width: 240px; aspect-ratio: 16 / 9 gives a 135px-tall box. It only works when at least one of width or height is automatic, and MDN lists it as Baseline widely available since September 2021.

object-fit is a different problem: a replaced element (an <img> or <video>) has its own picture with its own natural ratio, and the element's box may be a different shape. object-fit decides how the picture fills the box: fill (the default, stretch to fit and distort), contain (scale to fit inside, letterboxing the rest), cover (scale to cover the box, cropping the overflow), none (natural size, cropped) and scale-down (whichever of none or contain is smaller). object-position (default 50% 50%, centred) picks which part shows. MDN lists object-fit as Baseline widely available since January 2020.

Mental model: aspect-ratio shapes the frame; object-fit decides how the photo is placed in the frame: squashed to fit (fill), shrunk with borders (contain), or zoomed and trimmed (cover).

Boxes sized by a ratio
CSS
.video  { width: 240px; aspect-ratio: 16 / 9; background: #1e293b; color: #fff; margin-bottom: 8px; }
.square { width: 120px; aspect-ratio: 1; background: #fde68a; margin-bottom: 8px; }
.column { width: 300px; }
.column .fluid { aspect-ratio: 4 / 3; background: #bbf7d0; }
HTML
<div class="video">16 / 9</div>
<div class="square">1</div>
<div class="column"><div class="fluid">4 / 3, width auto</div></div>
Resultlive, in your browser
What’s happening
  1. .video has a fixed width of 240px and height: auto, so the ratio supplies the height: 240 × 9 / 16 = 135px.
  2. aspect-ratio: 1 (a single number means 1 / 1) makes a 120 × 120 square.
  3. .fluid has neither width nor height set. As a block, its width fills the 300px column, and the ratio gives 300 × 3 / 4 = 225px. Resize the column and the height follows, with no padding hacks.
AdvancedHeight-driven ratios and when the ratio is ignored

The ratio fills in whichever dimension is automatic. If you fix the height, the width follows. If you fix both, the ratio has nothing to do and is ignored. And because the ratio sets a preferred size, content that doesn't fit makes the box grow taller, unless the box has an overflow other than visible.

The ratio against content and fixed sizes
CSS
.h  { height: 100px; aspect-ratio: 2; background: #c7d2fe; margin-bottom: 8px; }
.both { width: 100px; height: 40px; aspect-ratio: 1; background: #fecaca; margin-bottom: 8px; }
.grow, .scroll { width: 100px; aspect-ratio: 1; background: #fde68a; margin-bottom: 8px; }
.scroll { overflow: auto; }
.grow i, .scroll i { display: block; height: 150px; }
HTML
<div class="h"></div>
<div class="both"></div>
<div class="grow"><i></i></div>
<div class="scroll"><i></i></div>
Resultlive, in your browser
What’s happening
  1. .h fixes height: 100px and leaves width auto, so the ratio of 2 makes it 200px wide, even though it's a block that would otherwise fill the line.
  2. .both sets width and height, so aspect-ratio: 1 is ignored: 100 × 40.
  3. .grow should be 100 × 100, but it holds 150px of content. The ratio's automatic minimum height lets content win, so the box grows to 150px.
  4. .scroll has overflow: auto, which removes that automatic minimum: the box stays 100px and the content scrolls. Use that (or min-height: 0) when the ratio must hold.
AdvancedImages: object-fit inside a fixed box

The image below is a 200 × 100 SVG (orange left half, blue right half), placed in 100 × 100 boxes. The box is the same size every time; only the way the picture is painted inside it changes.

One 2:1 picture in five square boxes
CSS
.row { display: flex; flex-wrap: wrap; gap: 12px; }
.row img { width: 100px; height: 100px; background: #f1f5f9; outline: 1px solid #94a3b8; }
.fill { object-fit: fill; }
.contain { object-fit: contain; }
.cover { object-fit: cover; }
.none { object-fit: none; }
.scale-down { object-fit: scale-down; }
HTML
<div class="row">
  <img class="fill" alt="fill" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='100'%3E%3Crect width='100' height='100' fill='%23f59e0b'/%3E%3Crect x='100' width='100' height='100' fill='%233b82f6'/%3E%3C/svg%3E">
  <img class="contain" alt="contain" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='100'%3E%3Crect width='100' height='100' fill='%23f59e0b'/%3E%3Crect x='100' width='100' height='100' fill='%233b82f6'/%3E%3C/svg%3E">
  <img class="cover" alt="cover" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='100'%3E%3Crect width='100' height='100' fill='%23f59e0b'/%3E%3Crect x='100' width='100' height='100' fill='%233b82f6'/%3E%3C/svg%3E">
  <img class="none" alt="none" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='100'%3E%3Crect width='100' height='100' fill='%23f59e0b'/%3E%3Crect x='100' width='100' height='100' fill='%233b82f6'/%3E%3C/svg%3E">
  <img class="scale-down" alt="scale-down" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='100'%3E%3Crect width='100' height='100' fill='%23f59e0b'/%3E%3Crect x='100' width='100' height='100' fill='%233b82f6'/%3E%3C/svg%3E">
</div>
Resultlive, in your browser
What’s happening
  1. Every image loaded with a natural size of 200 × 100, and every box is 100 × 100; object-fit never changes the box, only how the picture is painted into it. (A DOM test can't read painted pixels, so the painted results below are the spec's arithmetic; compare them with the live result.)
  2. fill stretches the picture to 100 × 100: both halves become squashed into 50px-wide strips.
  3. contain scales by min(100/200, 100/100) = 0.5: the picture is 100 × 50, centred, with 25px of empty box above and below.
  4. cover scales by max(0.5, 1) = 1: the picture stays 200 × 100 and is centred, so only the middle 100px shows, half orange and half blue.
  5. none doesn't scale at all, which here gives the same crop as cover. scale-down picks whichever of none and contain is smaller, so here it matches contain.
  6. Avatars and thumbnails use cover; logos and product shots that must not be cut use contain. Change object-position (say left center) to choose which part cover keeps.
Advancedwidth and height attributes prevent layout shift

The browser's stylesheet maps an image's width and height attributes to aspect-ratio: auto <width> / <height>. With width: 100%; height: auto in CSS, the browser can reserve the right height before the image has loaded, so the page doesn't jump when it arrives (a big part of the Cumulative Layout Shift metric). See Images, figure and media.

Space reserved before any image data
CSS
.responsive { width: 300px; height: auto; background: #e2e8f0; display: block; }
HTML
<img class="responsive" width="200" height="100" alt="">
<img class="responsive" id="bare" alt="">
Resultlive, in your browser
What’s happening
  1. The <img> has width="200" height="100" attributes but no src, so there's no image data: naturalWidth is 0.
  2. Its computed aspect-ratio is auto 200 / 100: "use the image's natural ratio once it's known, and 2:1 until then".
  3. CSS makes it 300px wide with height: auto, and the browser already reserves 150px of height. When the real image loads, nothing below it moves.
  4. The second <img> has the same CSS but no width/height attributes: its height is 0 until an image arrives, so everything below it would jump down when it loads.
AdvancedHistory: the padding-top ratio hack

Before 2021 the only way to make a fluid box keep a ratio was percentage padding, which (as the box model topic shows) is relative to the width. padding-top: 56.25% is 9/16 of the width. Content then had to be absolutely positioned inside. You'll still see it in older embeds.

The 16:9 padding hack
CSS
.ratio { position: relative; width: 320px; height: 0; padding-top: 56.25%; background: #1e293b; }
.ratio > div { position: absolute; inset: 0; color: #fff; padding: 8px; }
HTML
<div style="width: 320px"><div class="ratio"><div>16:9 by padding</div></div></div>
Resultlive, in your browser
What’s happening
  1. .ratio has no height of its own: height: 0. Its padding-top: 56.25% is 56.25% of the parent's 320px width, 180px, which is exactly 16:9.
  2. Anything placed normally would add to that height, so the content is absolutely positioned with inset: 0 to fill the padding area: 180px.
  3. Today aspect-ratio: 16 / 9 replaces all of this.
aspect-ratio and padding
CSS
.card { width: 300px; aspect-ratio: 3 / 2; padding: 10px; }

How tall is .card on screen under the default box-sizing, and under border-box?

Show answer

220px under content-box, 200px under border-box. The ratio applies to the box that box-sizing names.

The card under both box-sizing values
CSS
.card { width: 300px; aspect-ratio: 3 / 2; padding: 10px; background: #e9d5ff; margin-bottom: 8px; }
.bb { box-sizing: border-box; }
HTML
<div class="card" id="cb"></div>
<div class="card bb" id="bb"></div>
Resultlive, in your browser
What’s happening
  1. With content-box, the ratio sizes the content box: 300 wide means 200 tall. Padding adds 20px each way, so the card is 320 × 220, which isn't 3:2.
  2. With border-box, the ratio sizes the border box: 300 × 200, exactly 3:2, with the padding inside.
  3. One more reason for the border-box reset: with it, the visible box has the ratio you asked for.

Written fresh for interviews (there was no Google Doc for this one). Every live example was checked in headless Chrome 155 by scripts/check-demos.mjs.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.