Episode 1 — Fundamentals / 1.8 — CSS Layout Mastery

1.8.e — Positioning

In one sentence: CSS position removes elements from (or offsets them within) normal flow — relative shifts without disrupting siblings, absolute places relative to a positioned ancestor, fixed locks to the viewport, and sticky creates scroll-aware elements.

Navigation: ← 1.8.d — Combining Grid & Flex · 1.8.f — Stacking Context →


1. static — the default

Every element starts with position: static. Static elements follow normal flow — block elements stack vertically, inline elements flow horizontally. The top, right, bottom, left, and z-index properties have no effect on static elements.


2. relative — offset from normal position

.badge {
  position: relative;
  top: -8px;
  left: 12px;
}
  • The element is offset from where it would have been in normal flow.
  • Surrounding elements are not affected — the original space is preserved.
  • Creates a containing block for absolutely positioned descendants.
  • Creates a new stacking context when z-index is set.
Normal flow:        With position: relative; top: -8px; left: 12px;

┌──────┐            ┌──────┐
│  A   │            │  A   │
├──────┤            ├──────┤
│  B   │            │      │  ← original space preserved
├──────┤            ├──────┤
│  C   │         ┌──────┐
└──────┘         │  B   │  ← visually shifted
                 └──────┘
                    ├──────┤
                    │  C   │  ← not affected
                    └──────┘

3. absolute — relative to positioned ancestor

.parent {
  position: relative; /* establishes containing block */
}

.tooltip {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
}
  • The element is removed from normal flow — siblings behave as if it does not exist.
  • Positioned relative to the nearest ancestor that has position set to anything other than static (the containing block).
  • If no positioned ancestor exists, it positions relative to the initial containing block (the viewport/html).

Finding the containing block

The browser walks up the DOM tree looking for the first ancestor with:

  • position: relative, absolute, fixed, or sticky
  • A transform, filter, or perspective property (these also create containing blocks)

4. fixed — relative to the viewport

.floating-btn {
  position: fixed;
  bottom: 24px;
  right: 24px;
}
  • Removed from normal flow.
  • Positioned relative to the viewport — stays in place during scrolling.
  • Exception: if an ancestor has transform, filter, or perspective, the fixed element positions relative to that ancestor instead. This is a common source of bugs.

5. sticky — hybrid of relative and fixed

.sticky-header {
  position: sticky;
  top: 0;
}
  • Behaves like relative until the element reaches a scroll threshold (defined by top, bottom, etc.), then behaves like fixed within its scrolling container.
  • Stays in normal flow — surrounding elements are not affected.
  • Stops sticking when it reaches the end of its parent container.
Scrolling down:

  Before threshold:     At threshold:        Past parent:
  ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
  │   Header     │     │ ▓▓ Sticky ▓▓ │ ◄── stuck at top
  │   Content    │     │   Content    │     │   Content    │
  │ ▓▓ Sticky ▓▓ │     │   Content    │     │   (parent    │
  │   Content    │     │   Content    │     │    ended)    │
  │   Content    │     │   Content    │     │ ▓▓ Sticky ▓▓ │ ◄── scrolls away
  └──────────────┘     └──────────────┘     └──────────────┘

Sticky gotchas

  • overflow: hidden or overflow: auto on an ancestor can break sticky behavior (the sticky element's scrolling container becomes that ancestor).
  • Must set at least one threshold property (top, bottom, etc.) — without it, sticky has no threshold to trigger.

6. Offset properties: top / right / bottom / left

Position valueWhat offsets are relative to
relativeElement's normal position
absoluteContaining block edges
fixedViewport edges
stickyScroll threshold within scrolling container

Setting both top and bottom (or left and right) on an absolutely positioned element can stretch it:

.overlay {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  /* stretches to fill the containing block — same as inset: 0 */
}

The inset shorthand replaces all four:

.overlay { position: absolute; inset: 0; }
.partial { position: absolute; inset: 10px 20px; } /* top/bottom 10px, left/right 20px */

7. Practical examples

Sticky header

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: white;
}

Absolute badge on a card

.card {
  position: relative; /* containing block */
}

.card .badge {
  position: absolute;
  top: 12px;
  right: 12px;
  background: red;
  color: white;
  border-radius: 50%;
  padding: 4px 8px;
}
┌─────────────────────┐
│              [NEW] ◄─── absolute badge
│                     │
│   Card content      │
│                     │
└─────────────────────┘

Fixed floating action button

.fab {
  position: fixed;
  bottom: 24px;
  right: 24px;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  z-index: 200;
}

Absolute overlay (full-cover)

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 1000;
}

8. Comparison table

ValueIn flow?Offset relative toCreates stacking context?Use case
staticYesN/ANoDefault
relativeYesOwn normal positionWith z-indexContaining block, small offsets
absoluteNoPositioned ancestorWith z-indexTooltips, badges, dropdowns
fixedNoViewportAlwaysSticky headers, FABs, modals
stickyYesScroll thresholdAlwaysScroll-aware headers, sidebars

Explain-It Challenge

Explain without notes:

  1. What is a containing block, and how does the browser find it for an absolutely positioned element?
  2. Why might a position: fixed element stop being fixed relative to the viewport?
  3. What makes position: sticky different from just using JavaScript to toggle between relative and fixed?

Navigation: ← 1.8.d — Combining Grid & Flex · 1.8.f — Stacking Context →