Display and Positioning
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.
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;
}
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 */
}
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; }
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 do | What 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 |
- 1display controls two things: outer behavior (block/inline) and inner layout mode (flow/flex/grid)
- 2Inline elements ignore width, height, and vertical margin
- 3position: absolute positions relative to the nearest positioned ancestor — always set one explicitly
- 4position: fixed breaks when any ancestor has transform, filter, perspective, or contain
- 5z-index only competes within the same stacking context — higher values don't cross context boundaries