5 min read
On this page

Box Model & Positioning

Every Element Is a Box

Every HTML element generates a rectangular box. Understanding the box model is the foundation of all CSS layout. If you do not understand boxes, you cannot understand flexbox, grid, or any positioning scheme.

The box has four layers, from inside out:

  1. Content -- the actual text, image, or child elements
  2. Padding -- space between the content and the border
  3. Border -- the visible (or invisible) edge of the box
  4. Margin -- space between this box and neighboring boxes
.box {
  width: 200px;
  padding: 20px;
  border: 2px solid #333;
  margin: 16px;
}
|<------------ margin: 16px ------------>|
|  |<-------- border: 2px -------->|     |
|  |  |<---- padding: 20px ---->|  |     |
|  |  |  |<-- content: 200px ->|  |  |  |
|  |  |  |                     |  |  |  |

Box-Sizing: The First Rule

By default, width and height set the content size only. Padding and border are added on top:

/* Default: content-box */
.box {
  width: 200px;
  padding: 20px;
  border: 2px solid #333;
  /* Actual width: 200 + 20 + 20 + 2 + 2 = 244px */
}

This is counterintuitive. When you say "200px wide," you mean the whole box, not just the content. Fix this with border-box:

/* border-box: width includes padding and border */
.box {
  box-sizing: border-box;
  width: 200px;
  padding: 20px;
  border: 2px solid #333;
  /* Actual width: 200px (content shrinks to fit) */
}

Set this globally on every project:

*,
*::before,
*::after {
  box-sizing: border-box;
}

This is in every modern CSS reset. If you take one thing from this topic, take this.

Display Property

The display property controls how an element participates in layout.

block

Block elements take the full width of their parent and stack vertically.

.block-element {
  display: block;
}

Default block elements: <div>, <p>, <h1>-<h6>, <section>, <article>, <header>, <footer>, <ul>, <ol>, <li>, <form>.

Properties that work: width, height, margin (all sides), padding (all sides).

inline

Inline elements flow within text and only take as much width as their content needs.

.inline-element {
  display: inline;
}

Default inline elements: <span>, <a>, <strong>, <em>, <code>, <img>.

Properties that do not work on inline elements: width, height, margin-top, margin-bottom. Horizontal margins and all padding work, but vertical padding does not affect layout flow.

inline-block

Combines inline flow with block-level box behavior. The element flows inline but respects width, height, and vertical margins.

.badge {
  display: inline-block;
  padding: 4px 8px;
  width: 80px;        /* Works (unlike inline) */
  margin-top: 8px;    /* Works (unlike inline) */
  background: #e2e8f0;
  border-radius: 4px;
}

none

Removes the element from the layout entirely. It takes no space and is invisible. To hide visually but keep accessible to screen readers, use a .visually-hidden utility with position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0).

Margin Collapsing

Vertical margins between adjacent block elements collapse -- they do not add together. The larger margin wins.

.paragraph-a {
  margin-bottom: 24px;
}

.paragraph-b {
  margin-top: 16px;
}

/* Space between them: 24px (not 40px) */

Margin collapsing rules:

  • Only vertical margins collapse (top/bottom), never horizontal
  • Only margins of block-level elements in normal flow collapse
  • Margins do not collapse in flex or grid containers
  • Margins do not collapse when there is a border, padding, or content between them
/* Adding padding prevents collapse */
.parent {
  padding-top: 1px;  /* Prevents child margin from collapsing with parent */
}

This catches people off guard constantly. If spacing is not what you expect, check for margin collapsing.

Position Property

static (Default)

Elements are placed in normal document flow. top, right, bottom, left, and z-index have no effect.

relative

The element stays in normal flow but can be offset from its original position. Other elements are not affected -- they still see the element at its original position.

.nudged {
  position: relative;
  top: 10px;    /* Moves down 10px from where it would normally be */
  left: 20px;   /* Moves right 20px */
}

Most common use: creating a positioning context for absolutely positioned children.

absolute

The element is removed from normal flow. It is positioned relative to its nearest positioned ancestor (an ancestor with position set to anything other than static).

.parent {
  position: relative;  /* Creates positioning context */
}

.tooltip {
  position: absolute;
  top: 100%;       /* Just below the parent */
  left: 0;
  width: 200px;
  background: #1a1a2e;
  color: white;
  padding: 8px;
}

If no ancestor is positioned, the element is positioned relative to the initial containing block (usually the viewport).

fixed

The element is removed from normal flow and positioned relative to the viewport. It does not move when the page scrolls.

.sticky-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: white;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* Remember to add padding to body so content is not hidden behind it */
body {
  padding-top: 60px;
}

sticky

A hybrid of relative and fixed. The element is relative until it reaches a scroll threshold, then it sticks.

.section-header {
  position: sticky;
  top: 0;             /* Sticks when it reaches the top of the viewport */
  background: white;
  z-index: 10;
}

Sticky positioning requires a scrolling ancestor. It sticks within the bounds of its parent -- once the parent scrolls out of view, the sticky element goes with it.

Z-Index & Stacking Contexts

z-index controls which elements appear on top of others. It only works on positioned elements (anything with a position other than static) and flex/grid children.

.behind {
  position: relative;
  z-index: 1;
}

.in-front {
  position: relative;
  z-index: 2;   /* Appears on top of .behind */
}

Stacking Contexts

A stacking context is like a layer group. Elements within a stacking context are ordered among themselves, but the entire group is treated as a single unit by its parent context.

Things that create a new stacking context:

  • position: relative/absolute/fixed with a z-index value
  • opacity less than 1
  • transform, filter, perspective
  • isolation: isolate
/* This z-index: 9999 will NOT appear above the modal */
/* if the modal is in a higher stacking context */
.parent-a {
  position: relative;
  z-index: 1;
}

.child-inside-a {
  position: relative;
  z-index: 9999;   /* Only high within parent-a's context */
}

.modal-overlay {
  position: fixed;
  z-index: 10;     /* In the root stacking context, so it wins */
}

Instead of random z-index values, use a consistent scale with custom properties (--z-dropdown: 100, --z-sticky: 200, --z-modal: 400, etc.).

Common Pitfalls

  • Forgetting box-sizing: border-box: Without it, adding padding to a width: 100% element overflows its parent. Set it globally.
  • Trying to set width/height on inline elements: Inline elements ignore width and height. Use inline-block or block.
  • Not understanding margin collapse: Two adjacent 24px margins create 24px of space, not 48px. This is by design, but it surprises people.
  • Absolute positioning without a positioned parent: The element positions itself relative to the viewport (the initial containing block), not its visual parent. Add position: relative to the parent.
  • Z-index not working: Either the element is not positioned (z-index requires position other than static), or a stacking context is limiting it.
  • Z-index wars: Using z-index: 99999 indicates a stacking context problem. Use a consistent scale and understand which elements create stacking contexts.
  • Fixed elements and transform ancestors: A fixed element inside an ancestor with transform positions itself relative to that ancestor, not the viewport.
  • Using position: sticky without top/bottom: Sticky elements need a threshold. Without top: 0 (or similar), they never stick.

Key Takeaways

  • Every element is a box with content, padding, border, and margin
  • Always set box-sizing: border-box globally -- it makes sizing intuitive
  • Block elements stack vertically and take full width; inline elements flow with text
  • Vertical margins collapse between adjacent block elements
  • position: relative creates a positioning context for absolute children
  • position: absolute removes the element from flow and positions it relative to the nearest positioned ancestor
  • position: fixed locks to the viewport; position: sticky is a hybrid that sticks on scroll
  • Z-index only works on positioned elements, and stacking contexts limit its reach
  • Understanding the box model is prerequisite knowledge for flexbox, grid, and every layout technique