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 , ; the badge landed at point
. 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 . 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 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
staticleaves the box in its flow place and ignores offsets;relativeshifts the box by the offset but keeps its flow place.absoluteandfixedtake 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
positionvalue is other thanstatic; writingposition: relativewith no offset is exactly this job. stickyleaves 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.