Skip to content

Display and Positioning

beginner11 min read

Two Properties Control All of CSS Layout

Basically, every CSS layout question reduces to two properties: display (how an element participates in its parent's layout and how it lays out its children) and position (whether an element is removed from normal flow and how it's placed). Master these two and you understand where everything goes and why.

Mental Model

Think of display as choosing what type of vehicle an element is. Block elements are buses — they take up the full lane (width) and force the next vehicle onto a new line. Inline elements are motorcycles — they ride side by side and only take the space they need. Inline-block elements are cars — side by side like inline, but with controllable dimensions like block. Flex and grid are convoy coordinators — they control how their passengers (children) are arranged.

position is the navigation system. Static means follow the road (normal flow). Relative means drift slightly from your lane. Absolute means leave the road entirely and fly to specific coordinates relative to the nearest positioned ancestor. Fixed means hover at a fixed point on the screen. Sticky means follow the road until you hit a boundary, then stick there.

Display Modes

Block

/* Default for: div, p, h1-h6, section, article, header, footer, form */
.block {
  display: block;
  /* Takes full width available */
  /* Starts on a new line */
  /* Width, height, margin, padding all work as expected */
}

Inline

/* Default for: span, a, strong, em, code, img */
.inline {
  display: inline;
  /* Flows with text — sits side by side */
  /* Width and height have NO effect */
  /* Vertical margin and padding don't push other elements */
  /* Horizontal margin and padding work normally */
}

Inline-Block

.chip {
  display: inline-block;
  /* Sits in a line like inline */
  /* BUT: width, height, margin, and padding all work like block */
  padding: 4px 12px;
  border-radius: 9999px;
}
Common Trap

inline elements ignore width, height, and vertical margin. If you set margin-top: 20px on a <span>, nothing happens visually. This catches developers who expect all elements to respond to all properties equally. Switch to inline-block or display: flex to get full control.

None vs Contents

/* display: none — element removed from layout AND render tree */
.hidden { display: none; }
/* No space reserved. Not accessible to screen readers. */

/* display: contents — element's box disappears, children promoted */
.wrapper { display: contents; }
/* The wrapper div vanishes from layout. Its children behave as if
   they were direct children of the wrapper's parent. Useful for
   removing unnecessary wrapper divs in flex/grid layouts. */

The Two-Value Display Syntax

Turns out, display was always doing two jobs at once. Modern CSS makes that explicit by separating it into outer (how the element participates in its parent's layout) and inner (how it lays out its children):

.box { display: block flex; }   /* Block outside, flex inside (same as display: flex) */
.box { display: inline grid; }  /* Inline outside, grid inside (same as display: inline-grid) */
.box { display: block flow; }   /* Block outside, normal flow inside (same as display: block) */

Positioning

static (default)

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

relative

.nudged {
  position: relative;
  top: 10px;    /* Shifts down 10px from normal position */
  left: 20px;   /* Shifts right 20px from normal position */
  /* Original space is PRESERVED in the flow */
  /* Creates a positioning context for absolute children */
}

absolute

.tooltip {
  position: absolute;
  top: 100%;   /* Relative to nearest positioned ancestor */
  left: 0;
  /* REMOVED from normal flow — no space reserved */
  /* If no positioned ancestor: positions relative to ICB (initial containing block) */
}

The element is positioned relative to its nearest ancestor with position set to anything other than static:

.container {
  position: relative; /* This becomes the positioning context */
}

.overlay {
  position: absolute;
  top: 0;
  right: 0;
  /* Positioned relative to .container, not the viewport */
}

fixed

.navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  /* REMOVED from flow */
  /* Positioned relative to the viewport */
  /* Stays put during scroll */
}
Common Trap

position: fixed is NOT always relative to the viewport. If any ancestor has transform, perspective, filter, backdrop-filter, or contain: paint, the fixed element positions relative to that ancestor instead. This breaks fixed headers and modals in unexpected ways.

sticky

.table-header {
  position: sticky;
  top: 0;
  /* Behaves like relative UNTIL the scroll position hits the threshold */
  /* Then behaves like fixed — sticks in place */
  /* Bound by its parent container — unsticks when parent scrolls away */
}

Stacking Contexts and z-index

Here's where things get genuinely confusing. z-index only works on positioned elements (not static) and flex/grid children. But the real complexity is stacking contexts:

/* These properties create new stacking contexts: */
.creates-context {
  position: relative; z-index: 1; /* positioned + z-index */
  /* OR */
  opacity: 0.99;         /* Any opacity < 1 */
  /* OR */
  transform: translateZ(0); /* Any transform */
  /* OR */
  filter: blur(0);        /* Any filter */
  /* OR */
  isolation: isolate;     /* Explicitly creates one */
}

And this is the part that trips up even senior developers: within a stacking context, children's z-index values only compete with siblings in the same context. A z-index: 9999 inside a lower stacking context will appear behind a z-index: 1 in a higher stacking context:

.parent-a { position: relative; z-index: 1; }
.parent-b { position: relative; z-index: 2; }

/* This child will ALWAYS be behind .parent-b's children,
   no matter how high its z-index goes */
.parent-a .child { position: relative; z-index: 99999; }
Execution Trace
Normal flow
Block elements stack vertically, inline flow horizontally
Default layout with position: static
Relative
Element shifts but original space preserved
Creates positioning context for children
Absolute child
Removed from flow, anchored to relative parent
Other elements flow as if it doesn't exist
Stacking
z-index compared within same stacking context
Parent's z-index determines context's layer
Fixed
Anchored to viewport (unless ancestor has transform)
Stays in place during scroll

Production Scenario: Modal Layering

/* Problem: Modal appears behind sidebar */
.sidebar { position: relative; z-index: 100; }
.modal { position: fixed; z-index: 50; }
/* Modal is behind sidebar even though z-index seems sufficient */

/* Fix 1: Raise modal above all stacking contexts */
.modal { position: fixed; z-index: 1000; }

/* Fix 2: Use isolation on the sidebar */
.sidebar { position: relative; isolation: isolate; }
/* Sidebar's z-index now only affects its own context */

/* Fix 3 (best): Render modal at document root via portal */
/* In React: createPortal(modalContent, document.body) */
What developers doWhat they should do
Setting width/height on inline elements and expecting them to work
Inline elements size to their content. The CSS spec explicitly ignores width/height for inline formatting.
Use inline-block, block, or flex instead — inline ignores width/height
Using position: absolute without a positioned ancestor
Without a positioned ancestor, absolute elements position relative to the initial containing block (usually the viewport)
Always set position: relative on the intended container
Fighting z-index wars with ever-increasing values
z-index: 99999 inside a z-index: 1 context loses to z-index: 2 in a sibling context
Understand stacking contexts — z-index only competes within the same context
Expecting position: fixed to always anchor to the viewport
These CSS properties create a new containing block that captures fixed-positioned descendants
Check that no ancestor has transform, filter, perspective, or contain: paint
Quiz
What happens when you set margin-top: 20px on a <span> element?
Quiz
A div has position: fixed. Its grandparent has transform: scale(1). Where is the fixed div positioned relative to?
Quiz
.parent-a has z-index: 1, .parent-b has z-index: 2. A child inside .parent-a has z-index: 99999. Which appears on top?
Key Rules
  1. 1display controls two things: outer behavior (block/inline) and inner layout mode (flow/flex/grid)
  2. 2Inline elements ignore width, height, and vertical margin
  3. 3position: absolute positions relative to the nearest positioned ancestor — always set one explicitly
  4. 4position: fixed breaks when any ancestor has transform, filter, perspective, or contain
  5. 5z-index only competes within the same stacking context — higher values don't cross context boundaries