---
title: Positioning
source: 'https://academia.sh/en/courses/css-fundamentals/positioning'
course: 'Visual Presentation with CSS'
language: en
updated: '2026-08-17T18:09:16+00:00'
license: 'CC BY-SA 4.0'
---

# Positioning

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

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

```js
// 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=80$, $y=200$; the badge landed at point
$92, 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, 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:

```css
.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 = 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.**

```css
/* 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.
