The Fundamentals

Chapter 2 — The Fundamentals: Box Model, Display, and Positioning

Every element on a page is a rectangular box. The box model defines how that rectangle is sized. The display property defines how boxes relate to one another in flow. The position property defines whether a box participates in normal flow at all, and if not, where it lands instead. These three systems interact constantly — understanding each one precisely is the foundation of layout work.

1. The Box Model

Every box has four concentric layers. From inside out:

margin
border
padding
content
  • Content — where text and child elements render. Sized by width / height.
  • Padding — transparent space inside the border. Inherits the element's background.
  • Border — a drawn edge around the padding. Can have colour, style, width, radius.
  • Margin — transparent space outside the border. Always transparent — cannot have a background.

box-sizing — the most important CSS reset

By default, width and height size only the content area (box-sizing: content-box). Adding padding and border makes the element larger than the declared width — a constant source of layout bugs. The fix is universal:

content-box (default — avoid)
You declare width: 300px + padding: 20px + border: 2px
Total width = 300 + 40 + 4 = 344px The box is wider than you said. Every time you add padding or border you must mentally subtract from width. Breaks percentage-based layouts constantly.
border-box (use this — always)
You declare width: 300px + padding: 20px + border: 2px
Total width = 300px (padding + border eat into it) The box is exactly as wide as you said. Content shrinks to accommodate padding and border. Layouts become predictable.
/* The universal box-sizing reset — include in every project */ *, *::before, *::after { box-sizing: border-box; }

Margin collapse

Vertical margins between adjacent block elements collapse to the larger of the two values — they do not add together. This is intentional (consistent paragraph spacing) but surprises developers constantly.

p { margin-top: 24px; margin-bottom: 16px; } /* Two adjacent <p> elements: gap between them = 24px, not 40px */ /* The 24px "wins" over the 16px — they collapse */

Margin collapse rules:

  • Only happens vertically (top/bottom margins). Horizontal margins never collapse.
  • Only between block-level elements in normal flow. Flex/grid children never collapse margins.
  • Parent and first/last child margins collapse if no border, padding, or overflow separates them.
  • An empty block's top and bottom margins collapse into one.
  • Margins of different signs are added (a positive and negative cancel partially).
Stop margin collapse without hacks: Adding even padding-top: 1px or overflow: hidden to a parent prevents parent–child collapse. But the cleaner modern approach is to use Flexbox or Grid on the parent — flex/grid containers never collapse their children's margins.

Negative margins

/* Negative margins are valid and pull elements toward each other */ .sidebar { margin-top: -24px; /* pulls element UP 24px */ } /* Classic full-bleed trick: break out of a padded parent */ .full-bleed { margin-left: -32px; /* matches parent's padding-left */ margin-right: -32px; }

2. The display Property

display controls two things simultaneously: how the element itself participates in its parent's layout (the outer display type), and how it lays out its own children (the inner display type). The two-value syntax makes this explicit:

/* Two-value syntax (modern) */ .box { display: block flow; /* outer: block | inner: flow (normal flow) */ display: inline flow; /* outer: inline | inner: flow */ display: inline flow-root; /* outer: inline | inner: block formatting context */ display: block flex; /* outer: block | inner: flex */ display: inline flex; /* outer: inline | inner: flex */ display: block grid; /* outer: block | inner: grid */ } /* Shorthands (still fully supported) */ .box { display: block; /* = block flow */ display: inline; /* = inline flow */ display: inline-block; /* = inline flow-root */ display: flex; /* = block flex */ display: inline-flex; /* = inline flex */ display: grid; /* = block grid */ display: inline-grid; /* = inline grid */ }

Every display value explained

block
Takes up full parent width. Starts a new line. Respects width, height, margin, padding in all directions. Default for div, p, h1–h6, section, article, ul, ol.
inline
Flows with text. No line break. Width/height ignored. Horizontal margin/padding work; vertical margin/padding don't affect layout flow. Default for span, a, strong, em.
inline-block
Inline on the outside (no line break, flows with text), block on the inside (respects width, height, all margin/padding). Old-school approach for horizontal nav items.
flex
Block-level flex container. Children become flex items, laid out in one axis. The modern replacement for inline-block horizontal layouts.
inline-flex
Inline-level flex container. Useful for icon+text combos inside text flow — the container stays inline but its children are flex-laid-out.
grid
Block-level grid container. Children become grid items placed on a two-dimensional grid. The most powerful CSS layout tool.
inline-grid
Inline-level grid container. Uncommon — used when a grid needs to size to its content and flow inline with surrounding text.
flow-root
Creates a new block formatting context (BFC) without being flex or grid. Reliably contains floats and prevents margin collapse from leaking out. Replaced the old overflow: hidden clearfix hack.
contents
The element itself generates no box — its children are treated as if they are direct children of the parent. Useful for semantic wrapper elements in flex/grid layouts. Accessibility warning: removes the element from the accessibility tree in some browsers.
none
Removes the element from the document entirely — no box, no space, invisible to accessibility tools. Different from visibility: hidden which hides visually but preserves the space and keeps the element in the accessibility tree.
table / table-*
Emulates HTML table behaviour on non-table elements. Rarely needed in modern CSS — grid handles these patterns better — but useful for legacy email templates.
list-item
Block with a marker box (bullet/number). Default for li. Rarely set explicitly but useful when you want non-li elements to render like list items.

Block Formatting Context (BFC)

A BFC is an independent layout region. Elements inside don't affect elements outside, and vice versa. Understanding what creates a BFC explains many CSS "quirks":

/* Things that create a new BFC */ /* - display: flow-root (cleanest) */ /* - overflow: hidden / scroll / auto (legacy clearfix) */ /* - display: flex / grid / inline-block (on the container) */ /* - position: absolute / fixed (implicit BFC) */ /* - float: left / right (implicit BFC) */ /* BFC use cases: */ /* 1. Contain floated children */ .container { display: flow-root; } /* 2. Prevent margin collapse escaping the parent */ .card { overflow: hidden; } /* older approach */ .card { display: flow-root; } /* better */ /* 3. Stop text from wrapping around a float */ .text-col { overflow: hidden; }

3. Positioning

The position property removes an element from normal flow (or modifies its position within it) using the offset properties top, right, bottom, left, and inset.

Value In normal flow? Offset reference Creates stacking context? Typical use
static Yes None — offsets ignored No Default. Every element is static unless changed.
relative Yes (holds its space) Its own normal-flow position If z-index ≠ auto Nudge from normal position. More importantly: establish a containing block for absolute children.
absolute No (removed) Nearest positioned ancestor (non-static) If z-index ≠ auto Tooltips, dropdowns, badges, overlays — anything that needs to float over other content.
fixed No (removed) Viewport (initial containing block) Yes (always) Sticky nav bars, cookie banners, floating chat buttons. Unaffected by scroll.
sticky Yes — until threshold Nearest scrolling ancestor Yes (always) Table headers, section headers that stick on scroll. Behaves like relative until the offset threshold is hit.

The containing block — the key to understanding absolute positioning

An absolutely positioned element is placed relative to its containing block — the nearest ancestor that has a position value other than static. If no such ancestor exists, it falls back to the initial containing block (the viewport). This is why position: relative is added to a parent even when you don't want to move the parent itself:

.card { position: relative; /* makes .card the containing block */ } .card__badge { position: absolute; top: -8px; right: -8px; /* Positioned relative to .card's border edge, not the viewport */ }
transform creates a containing block too. Any element with a transform, filter, or perspective value other than none becomes the containing block for absolutely positioned descendants — even without position: relative. This catches developers off guard when a fixed element stops being fixed because an ancestor has a CSS transform applied to it.

The inset shorthand

/* inset is a shorthand for top / right / bottom / left */ .overlay { position: absolute; inset: 0; /* top:0; right:0; bottom:0; left:0 — fills container */ } .tooltip { position: absolute; inset: auto 0 0 auto; /* top:auto right:0 bottom:0 left:auto */ }

Centering with absolute positioning

/* Classic approach — requires known dimensions */ .modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); /* pull back by 50% of OWN size */ } /* Modern approach with inset + margin: auto */ .modal { position: absolute; inset: 0; margin: auto; width: 400px; /* must have explicit dimensions */ height: fit-content; }

sticky — how it actually works

.section-header { position: sticky; top: 0; /* sticks when this distance from the scroll container top is reached */ } /* Common sticky failures: */ /* 1. No offset defined — sticky with no top/bottom/left/right never sticks */ /* 2. overflow: hidden/auto on any ancestor — clips sticky and breaks it */ /* 3. Parent is too short — sticky only works within its parent's bounds */ /* 4. height: 100% on the parent — parent is the full page, sticky never fires */

4. z-index and Stacking Contexts

z-index controls which element appears on top when boxes overlap. But it only works on elements that are positioned (anything except static) or are flex/grid items. The critical detail most developers miss: z-index values only compare within the same stacking context.

What creates a stacking context

  • The root element (<html>) — the base stacking context
  • position: relative/absolute + z-index other than auto
  • position: fixed or sticky (always)
  • opacity less than 1
  • transform, filter, perspective, clip-path (non-none)
  • isolation: isolate — the clean, explicit way to create one
  • will-change with certain properties
  • Flex/grid items with z-index other than auto
Root stacking context
z-index: auto
Parent (creates own context — opacity: 0.9)
z-index: 10
Child with z-index: 9999
z-index: 9999 ← useless

In the diagram above, the child's z-index: 9999 only competes within the parent's stacking context. The parent itself has z-index: 10 in the root context. No matter how high the child's z-index goes, it cannot render above something at the root context level with z-index: 11.

/* The stacking context trap */ .parent { position: relative; z-index: 1; /* creates a stacking context */ opacity: 0.99; /* ALSO creates a stacking context */ } .parent .dropdown { position: absolute; z-index: 9999; /* trapped inside parent's context */ /* Will never render above another element at parent's peer level with z-index: 2 */ } /* Fix: use isolation: isolate to clearly control context creation */ .parent { isolation: isolate; /* explicit, readable, no side effects */ }
Debugging z-index issues: In Chrome DevTools, the Layers panel (More Tools → Layers) shows every stacking context as a separate layer. When a z-index value seems to have no effect, open the Layers panel and find the element — it will show you which context it belongs to and who its siblings are in that context.

5. overflow

overflow controls what happens when content is larger than its box. It is a shorthand for overflow-x and overflow-y.

.box { overflow: visible; /* default — content spills out, no scroll, no clip */ overflow: hidden; /* clips content — also creates a BFC */ overflow: scroll; /* always shows scrollbars (even if not needed) */ overflow: auto; /* scrollbar only when needed — almost always what you want */ overflow: clip; /* clips like hidden but does NOT create a BFC */ /* Axis-specific */ overflow-x: auto; overflow-y: hidden; }
overflow: hidden breaks position: sticky. If any ancestor of a sticky element has overflow: hidden, overflow: auto, or overflow: scroll, the sticky element will not stick. This is the most common reason sticky mysteriously stops working. Use overflow: clip if you need to clip without creating a scroll container.

Chapter Summary

ConceptKey point
box-sizingAlways set *, *::before, *::after { box-sizing: border-box }. With border-box, declared width includes padding and border — no mental arithmetic needed.
margin collapseVertical block margins collapse to the larger value. Flex/grid containers prevent collapse. display: flow-root prevents parent–child collapse without side effects.
displayControls outer type (how the element sits in its parent's flow) and inner type (how it lays out its own children). flex/grid set both at once.
flow-rootCreates a BFC cleanly. Use it to contain floats or prevent margin collapse escaping a parent, instead of the old overflow: hidden hack.
position: relativeDoesn't move the element visually unless you add offsets. Main purpose: establish a containing block for absolutely positioned children.
position: absoluteRemoved from flow. Positioned relative to nearest non-static ancestor. If none, falls back to viewport.
transform breaks fixedAny ancestor with a CSS transform becomes the containing block for fixed/absolute children. Fixed elements stop being fixed relative to the viewport.
sticky failuresMust have an offset. Any ancestor with overflow: hidden/auto/scroll breaks it. Only works within its parent's bounds.
Stacking contextsz-index only compares within the same stacking context. An element can't escape its parent's context no matter how high its z-index. Use isolation: isolate to create contexts deliberately.
Exercises
  1. Box model maths: Create a div with width: 200px, padding: 20px, border: 5px solid. Without box-sizing: border-box, how wide is it? With it? Verify in DevTools by hovering the element in the Elements panel.
  2. Margin collapse: Create two adjacent <p> elements each with margin: 24px 0. Measure the gap. Now wrap them in a <div> and add display: flex; flex-direction: column to the wrapper. Measure again. Explain the difference.
  3. Positioned badge: Build a card with a "NEW" badge positioned at the top-right corner using position: absolute. Ensure the badge stays anchored to the card, not the viewport.
  4. Sticky header: Create a page with multiple sections each 400px tall. Give each section a heading with position: sticky; top: 0. Then add overflow: hidden to the section. Observe what breaks and fix it.
  5. Stacking context trap: Create two overlapping absolutely positioned divs. Give one z-index: 1 and its child z-index: 999. Give the other z-index: 2. Confirm that z-index: 999 cannot beat z-index: 2.
Next: Chapter 3 — Typography, Color, and Backgrounds. A deep dive into font loading and the font stack, all font properties, colour systems (hex, rgb, hsl, oklch), gradients, background-image, background-size and position, and blending modes.