Skip to content
academia.sh

Lesson 14 / 26

Positioning

Where static, relative, absolute, fixed, and sticky positioning put a box; the containing block concept and what the offset properties are measured against.

Contents

Floating took a box out of flow but did not let its destination be chosen; the box was only pushed to one edge. The station page has places where this is not enough: a status badge sitting in a card’s corner, a measurement table’s column headers staying visible while scrolling.

This lesson defines the position property’s five values. Each value has two questions: does the box stay in flow, and what box are its offset values measured against?

Five Values, Two Questions

// position.mjs — computes where each of the five positioning values puts a box
const CONTAINER = { name: "main",     x: 40, y: 100, width: 640,  height: 800 };
const CARD       = { name: "card",     x: 80, y: 200, width: 400,  height: 240 }; // its place in flow
const VIEWPORT    = { name: "viewport", x: 0,  y: 0,   width: 1024, height: 768 };

function place({ position, flowPosition, containingBlock, offset, scrollY = 0 }) {
  const { top = null, left = null, width = 120, height = 40 } = offset;
  switch (position) {
    case "static":
      return { x: flowPosition.x, y: flowPosition.y, width: flowPosition.width, height: flowPosition.height, inFlow: true };
    case "relative":
      return { x: flowPosition.x + (left ?? 0), y: flowPosition.y + (top ?? 0),
               width: flowPosition.width, height: flowPosition.height, inFlow: true };
    case "absolute":
      return { x: containingBlock.x + (left ?? 0), y: containingBlock.y + (top ?? 0), width, height, inFlow: false };
    case "fixed":
      return { x: VIEWPORT.x + (left ?? 0), y: VIEWPORT.y + (top ?? 0), width, height, inFlow: false };
    case "sticky": {
      const threshold = top ?? 0;
      const flowedY = flowPosition.y - scrollY;
      return { x: flowPosition.x, y: Math.max(threshold, flowedY),
               width: flowPosition.width, height: flowPosition.height, inFlow: true, stuck: flowedY < threshold };
    }
  }
}

const badge = { x: 0, y: 0, width: 120, height: 40 };
console.log("containing block: main  x=40 y=100 640x800");
console.log("flow position    : badge x=0  y=0   120x40\n");

for (const position of ["static", "relative", "absolute", "fixed"]) {
  const r = place({ position, flowPosition: badge, containingBlock: CARD, offset: { top: 16, left: 12 } });
  console.log(`${position.padEnd(9)} -> x=${String(r.x).padStart(4)} y=${String(r.y).padStart(4)}  ${r.width}x${r.height}  inFlow=${r.inFlow}`);
}
console.log("  (for absolute, containing block = card: x=80 y=200)");

console.log("\n--- sticky: heading, threshold top=0, as scroll advances ---");
const heading = { x: 40, y: 300, width: 640, height: 48 };
for (const scrollY of [0, 200, 300, 400, 520]) {
  const r = place({ position: "sticky", flowPosition: heading, containingBlock: CONTAINER, offset: { top: 0 }, scrollY });
  console.log(`scroll=${String(scrollY).padStart(3)} -> y on screen=${String(r.y).padStart(3)}  stuck=${r.stuck}`);
}
containing block: main  x=40 y=100 640x800
flow position    : badge x=0  y=0   120x40

static    -> x=   0 y=   0  120x40  inFlow=true
relative  -> x=  12 y=  16  120x40  inFlow=true
absolute  -> x=  92 y= 216  120x40  inFlow=false
fixed     -> x=  12 y=  16  120x40  inFlow=false
  (for absolute, containing block = card: x=80 y=200)

--- sticky: heading, threshold top=0, as scroll advances ---
scroll=  0 -> y on screen=300  stuck=false
scroll=200 -> y on screen=100  stuck=false
scroll=300 -> y on screen=  0  stuck=false
scroll=400 -> y on screen=  0  stuck=true
scroll=520 -> y on screen=  0  stuck=true

Reading the Values

static — the initial value. The box stays in its normal-flow place, and the offset properties (top, right, bottom, left; their logical counterparts inset-block-start and its relatives) are ignored. In the output, the position did not change even though an offset was given.

relative — the box is shifted from its flow position by the offset but keeps its flow place. In the output, the box shifted 12 and 16 units; its neighbors are unaffected by this shift, and the box’s old place stays empty. This is both the power and the limit of relative positioning: it makes a visual correction, it does not rearrange layout.

absolute — the box is taken out of flow, and offsets are measured against the containing block. In the output, the card was at x=80x=80, y=200y=200; the badge landed at point 92,21692, 216. Since the box left flow, its width no longer fills its container either — it shrinks to fit its content.

fixed — the box is taken out of flow, and offsets are measured against the viewport. In the output, wherever the card was, the box landed at point 12,1612, 16. When the page is scrolled, the box stays fixed on screen.

sticky — it sits between the two, and is shown in the output’s second block.

How the Containing Block Is Determined

The most commonly misunderstood part of absolute positioning is what the “containing block” is. The rule is this: the box is placed relative to its nearest positioned ancestor. Positioned means an ancestor whose position value is something other than static.

If no ancestor is positioned, the containing block is the document’s first box; the box is placed relative to the entire page — this is usually the unwanted result.

This gives rise to a standard pairing:

.card         { position: relative; }
.card .badge  { position: absolute; inset-block-start: 8px; inset-inline-end: 8px; }

The card is given position: relative but no offset is written. The goal is not to shift the card, it is to turn it into a containing block. The badge now sits at the card’s corner.

The shorthand for offsets is inset: inset: 0 zeroes all four offsets and stretches an absolutely positioned box to exactly fill its containing block.

When two opposite offsets are given together — inset-inline-start: 0 and inset-inline-end: 0 — the box’s width is forced to the distance between them. This is a way of giving a width without writing width.

Sticky Positioning Is a Threshold Behavior

position: sticky leaves the box in flow. The box stays in its normal place — until, during scrolling, the declared threshold is reached. From that point on, the box sticks at the threshold.

The output’s second block shows this step by step. The heading was at y=300y = 300 in flow, and the threshold was given as inset-block-start: 0. Once scrolling advanced 200 units, the heading moved to point 100 on screen — still moving together with flow. Once scrolling reached 300, the heading touched the threshold; at 400 and 520 it stayed at point 0 on screen, meaning it had stuck.

Two conditions are required, and both are often overlooked.

A threshold must be declared. position: sticky alone does nothing; at least one offset property must be written.

Sticking is confined to the containing box’s bounds. The box does not leave its container; once the container is scrolled past, the box goes with it. This is what lets a section heading stick only while its own section is on screen — that is usually the wanted behavior.

There is also an obstacle: a sticky box sticks relative to a scrollable ancestor, if it has one. Having overflow: hidden on one of the ancestors in between silently stops sticking. This is the first place to check on a box that fails to stick.

The Cost of Leaving Flow

Absolute and fixed positioning take the box out of flow. The cost is that the box takes no part in its container’s height — the same problem seen with floating.

The result is that absolutely positioned content can overlap the content beneath it. This is why absolute positioning is used not for something whose size changes together with content, but for something whose size is fixed or whose overlapping is not a problem: a badge, a close button, a decorative marker.

Building a page’s main layout with positioning means rewriting by hand every automatic behavior flow provides. The general rule is: layout is built with flow; positioning is for individual exceptions flow cannot give.

/* station.css — step 13: positioning */
.card { position: relative; }

.card .status-badge {
  position: absolute;
  inset-block-start: 8px;
  inset-inline-end: 8px;
}

.measurement-table thead th {
  position: sticky;
  inset-block-start: 0;
  background-color: #f5f7f8;
}

.skip-link {
  position: absolute;
  inset-block-start: -100px;
}

.skip-link:focus {
  position: fixed;
  inset-block-start: 8px;
  inset-inline-start: 8px;
}

The background color on the table header is required: the sticky header is drawn over the rows passing beneath it, and if it stays transparent, two lines of text get read on top of each other.

The last two rules are an accessibility pattern. The skip link to content sits outside the viewport; when keyboard focus reaches it, it switches to a fixed position and becomes visible. The rule could not be written with display: none — an element producing no box cannot be focused.

Summary

  • static leaves the box in its flow place and ignores offsets; relative shifts the box by the offset but keeps its flow place.
  • absolute and fixed take the box out of flow; the first measures against the nearest positioned ancestor, the second against the viewport.
  • For absolute positioning, the containing block is the nearest ancestor whose position value is other than static; writing position: relative with no offset is exactly this job.
  • sticky leaves the box in flow and holds it in place once the declared threshold is reached; at least one offset must be declared, and sticking is bounded by the containing box.
  • A box taken out of flow contributes nothing to its container’s height; this is why positioning is used not for the main layout but for exceptions flow cannot give.

Next Step

Positioning can make boxes overlap: a badge over a card, a stuck header over rows. So when two boxes overlap at the same point, which one is drawn on top? Writing z-index does not always work — sometimes a higher value ends up underneath. The next lesson defines paint order and the stacking context that is the reason for this behavior.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close