Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~30 min· topic 2 of 5

Topic 6.2

Positioning

In one line

The position property decides how a box is placed: static follows normal flow, relative shifts a box but keeps its space, absolute and fixed take it out of flow and place it against a containing block, and sticky lets it scroll until it hits an edge and then stay there.

Think of it like this

Seats in a cinema. Most people sit in the seat on their ticket: that's static, the normal order. Someone who leans over to the next seat while keeping their own is relative: their seat stays empty and theirs. A person standing against the wall of the room, not using any seat, is absolute: they're placed against the room's walls and the rows ignore them. The exit sign glued to the screen frame is fixed: it stays in view whatever happens. A popcorn tray that slides along a rail and stops at the end is sticky.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Positioned element
An element whose position is anything except static. Only these use top, right, bottom and left.
Offset properties
top, right, bottom, left and the shorthand inset: how far a positioned box is placed from an edge.
Containing block
The rectangle a box's position and percentage sizes are measured against.
Padding box
The area inside an element's border: content plus padding. Absolute children are placed against it.
Initial containing block
The viewport-sized rectangle at the top of the page, used when no ancestor is positioned.
Viewport
The visible area of the browser window where the page is shown.
Static position
Where an absolute box would have been in normal flow. It stays there if its offsets are auto.
Scroll container
An element that can scroll its content, because its overflow is auto, scroll or hidden.
Sticky threshold
The offset, like top: 0, at which a sticky box stops scrolling and sticks.

Step by step

01Static: offsets do nothing

Write top: 50px on a normal paragraph. Nothing happens. The paragraph is position: static, and static boxes ignore offsets. The declaration is valid, so no error appears; it simply has no effect.

style.csswhole filecss
p {
  top: 50px; /* ignored: p is position: static */
}

02Relative: nudge, keep the seat

Add position: relative. Now top: 10px; left: 20px draws the box 10px lower and 20px further right. The box's original space is still kept, so the next element doesn't move up or sideways.

style.csswhole filecss
.nudge {
  position: relative;
  top: 10px;
  left: 20px;
}

03Absolute: pin it inside a positioned parent

To put a badge in a card's corner, make the card the containing block with position: relative, then give the badge position: absolute and offsets. The badge leaves the flow, so the card's text doesn't make room for it.

style.csswhole filecss
.card {
  position: relative;  /* the containing block */
}
.card .badge {
  position: absolute;
  top: 8px;
  right: 8px;
}

04Find the containing block

When an absolute box lands somewhere unexpected, walk up its ancestors: the first one that is positioned (or has a transform, filter and the like) is its containing block. If none is, the box is placed against the initial containing block, at the top of the page.

Find the containing blockdiagram
Rendering diagram…

05Fixed: pinned to the screen

position: fixed; bottom: 16px; right: 16px keeps a button in the corner of the window while the page scrolls. Fixed boxes are out of flow, so a fixed header covers the top of the page; add matching padding-top to the body (or scroll-padding-top for anchor links) so it doesn't hide content.

style.csswhole filecss
.to-top {
  position: fixed;
  right: 16px;
  bottom: 16px;
}

06Sticky: scroll, then stick

A table header or list section title with position: sticky; top: 0 scrolls up with the page until it reaches the top of its scroll container, then stays there until its parent section scrolls away. Give it a background, or the content scrolling underneath will show through.

style.csswhole filecss
.section-title {
  position: sticky;
  top: 0;
  background: white; /* hide what scrolls underneath */
}

Try it yourself

  1. 1

    Remove the containing block

    In the first example, delete position: relative from .stage. Predict where .abs goes and what the console prints for it. (It's now placed against the initial containing block, the top right of the whole preview, so its left changes with the preview's width.)

  2. 2

    Stretch with insets

    In the second example, replace the badge's width/height with inset: 4px and remove top: 0; right: 0. Predict the badge's size. (It fills the containing block minus 4px on each side: both left and right are set, so the width is worked out from them.)

  3. 3

    Make sticky stop working

    In the fourth example, add overflow: hidden to section. Predict the console output. (The section is now the headers' scroll container, but it never scrolls, so the headers scroll away like normal boxes: Fruits at −100 when scrolled 100.)

Code & diagrams

Relative keeps its space; absolute leaves the flow New tab

`.rel` is drawn 20px right and 10px down, but `.next` still starts at 40px: the relative box's original space is kept. `.abs` is out of flow and sits 10px from the stage's top and right edges: 280 − 10 − 90 = 180.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

rel left 20 top 10
next left 0 top 40
abs left 180 top 10
The nearest positioned ancestor wins New tab

First the middle box is static, so the badge skips it and uses `.outer`'s padding box: right edge at 240 + 40 = 280, minus 24. Then the script makes `.middle` positioned and the badge moves into its corner. `offsetParent` is JavaScript's name for the nearest positioned ancestor.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

containing block: outer | left 256 top 0
containing block: middle | left 236 top 20
A transform breaks position: fixed New tab

`.f1` is pinned to the viewport's top-left corner. `.f2` has the same CSS, but its parent has a `transform`, which makes the parent its containing block: it lands at the parent's corner (60px in, 50px down) and would scroll with the page.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

.f1 left 0 top 0
.f2 left 60 top 50
Sticky headers inside a scrolling list New tab

At 100px the Fruits header has stuck at 0 while its items scroll under it. Its section is 230px tall (30 + 5 × 40), so at 300px the section has scrolled away and pushed its header up and out (−100), while the Vegetables header has stuck in its place. Scroll the list yourself to watch the hand-over.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

scrolled 0 | Fruits at 0 | Vegetables at 230
scrolled 100 | Fruits at 0 | Vegetables at 130
scrolled 300 | Fruits at -100 | Vegetables at 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget to position the parent

Write .card .close { position: absolute; top: 8px; right: 8px; } but leave .card with the default position: static.

terminal
$ the page shows
── what you'll see ──
Every close button jumps to the top-right corner of the page and they all sit on top of each other.

Break #2

A sticky header that won't stick

Give a header position: sticky without top, or keep top: 0 but give an ancestor between it and the page overflow: hidden.

terminal
$ the page shows
── what you'll see ──
The header scrolls away with the content as if it were a normal element. No error appears.

Break #3

A fixed modal that scrolls with the page

Put a position: fixed modal inside a wrapper that has an entrance animation using transform (or a filter: blur()).

terminal
$ the page shows
── what you'll see ──
The modal is positioned relative to the wrapper, not the window, and moves away when the page scrolls.

Myth vs fact

Myth

position: absolute positions an element relative to its parent.

Fact

It positions it relative to the nearest positioned ancestor (or one with a transform or filter), which may be the grandparent or the page. The parent only counts if it is positioned.

Myth

position: relative with no offsets does nothing.

Fact

It doesn't move the box, but it makes the box a containing block for absolute children, lets z-index apply to it, and draws it above non-positioned siblings.

Myth

position: fixed always sticks to the window.

Fact

A transformed, filtered or contain: paint ancestor captures it. Fixed boxes also act differently in some mobile browsers when the on-screen keyboard opens or the address bar hides.

Myth

Sticky is broken in some browsers.

Fact

Sticky works in every modern browser. Almost every "broken sticky" is an ancestor with overflow set, a missing top, or a parent no taller than the sticky box.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Positioning changes layout cost. Relative offsets are applied after layout, so animating top/left on a relative box still triggers layout each frame; transform: translate() moves the box at the compositing stage and is much cheaper (Phase 10).

  • ▸

    For absolute boxes, the spec solves an equation per axis: left + margin-left + border + padding + width + ... + right = containing block width. With left, right and width all set, the box is over-constrained and right is ignored in left-to-right text. With left, right set and width: auto, the width is computed from them. This is why inset: 0 stretches a box.

  • ▸

    CSS anchor positioning (anchor-name, position-anchor, anchor()) lets a tooltip or menu be positioned against any element, not only its containing block, and flip when it would leave the screen. It shipped in Chromium in 2024 and in Safari in 2025; check current support in all browsers before relying on it without a fallback.

  • ▸

    When debugging, element.offsetParent in the console gives the nearest positioned ancestor (roughly the containing block of an absolute child). It is null for position: fixed boxes and for elements with display: none. Firefox DevTools also marks offsets on a static element as inactive and explains why.

Remember this

  1. 1

    position: static is the default. The box sits wherever normal flow puts it, and the offset properties top, right, bottom and left (and their shorthand inset) are ignored. A box with any other value is called positioned, and the offsets now mean something. inset: 10px is short for all four sides; inset: 0 means top: 0; right: 0; bottom: 0; left: 0.

  2. 2

    position: relative keeps the box in flow, in its original place, then shifts how it is drawn by the offsets. top: 10px moves it 10px down (away from the top), left: 20px moves it 20px right. Its original space stays reserved, so siblings don't move, and the shifted box can overlap them. The most common use of relative isn't moving anything: it's making the box a containing block for absolutely positioned children.

  3. 3

    position: absolute takes the box out of flow: its siblings close up as if it weren't there. It is then placed against its containing block, which for an absolute box is the padding box (the area inside the border) of the nearest positioned ancestor (any ancestor whose position isn't static). If there isn't one, it's the initial containing block, a rectangle the size of the viewport at the top of the page. top: 10px; right: 10px puts the box 10px from that block's top and right edges. An absolute box with width: auto shrinks to fit its content instead of filling the width.

  4. 4

    If an absolute box has offsets of auto on an axis, it stays at its static position: where it would have been in normal flow, but still taking no space. If it has both left and right (or inset: 0), it stretches between them, and with a fixed width plus margin: auto it centres itself (Topic 6.5). Percentages in top/bottom are measured against the containing block's height, and in left/right against its width.

  5. 5

    position: fixed works like absolute, but the containing block is the viewport (the visible window), so the box stays in the same place on screen while the page scrolls: headers, chat buttons, cookie banners. One famous exception: if any ancestor has a transform, filter, perspective, backdrop-filter, contain: paint or will-change: transform, that ancestor becomes the containing block instead, and the "fixed" box scrolls with it.

  6. 6

    position: sticky is a hybrid. The box stays in flow and scrolls normally until it reaches a threshold you set, such as top: 0, in its nearest scroll container (an ancestor that scrolls, or the page). Then it sticks there while the rest of the content scrolls under it. It can never leave its parent: when the bottom of the parent scrolls past, the sticky box is pushed away with it. That's how section headers in a long list replace each other. Sticky needs a threshold (top, bottom, left or right); without one it acts like relative.

Explain it without notes

01

Explain the five values of position and whether each keeps the box in normal flow.

02

What is the containing block of an absolutely positioned element?

03

Why might a position: fixed element scroll with the page?

04

How does position: sticky work, and what are the common reasons it fails?

05

Why do developers put position: relative on a parent without any offsets?

Practice

01

A .photo card holds an <img> and a <span class="tag">New</span>. Put the tag 10px from the card's bottom-left corner.

02

Make an overlay that exactly covers its .hero parent with a half-transparent black layer.

03

Make a page header that stays visible at the top of the window while the page scrolls, without hiding the first line of content.

Trade-offs

  • ↔

    absolute gives exact placement but takes the box out of flow, so the parent no longer grows to fit it and long text can overflow or overlap. Prefer Flexbox or Grid for layout and keep absolute for decorations, badges and overlays.

  • ↔

    A sticky header keeps its space and needs no padding hacks, but it only works within its parent and scroll container; a fixed header works anywhere but covers content unless you reserve space, and costs screen height on small phones.

Done when you can

  • I can explain static, relative, absolute, fixed and sticky, and which ones stay in flow.

  • I can find an absolute box's containing block and fix one that lands in the wrong place.

  • I can use inset to stretch an overlay over its parent.

  • I know which ancestor properties break position: fixed.

  • I can debug a sticky header that doesn't stick.