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:
- Content -- the actual text, image, or child elements
- Padding -- space between the content and the border
- Border -- the visible (or invisible) edge of the box
- 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/fixedwith az-indexvalueopacityless than 1transform,filter,perspectiveisolation: 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 awidth: 100%element overflows its parent. Set it globally. - Trying to set width/height on inline elements: Inline elements ignore
widthandheight. Useinline-blockorblock. - 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: relativeto the parent. - Z-index not working: Either the element is not positioned (z-index requires
positionother thanstatic), or a stacking context is limiting it. - Z-index wars: Using
z-index: 99999indicates 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
transformpositions itself relative to that ancestor, not the viewport. - Using
position: stickywithouttop/bottom: Sticky elements need a threshold. Withouttop: 0(or similar), they never stick.
Key Takeaways
- Every element is a box with content, padding, border, and margin
- Always set
box-sizing: border-boxglobally -- 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: relativecreates a positioning context for absolute childrenposition: absoluteremoves the element from flow and positions it relative to the nearest positioned ancestorposition: fixedlocks to the viewport;position: stickyis 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