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
positionis anything exceptstatic. Only these usetop,right,bottomandleft. - Offset properties
top,right,bottom,leftand the shorthandinset: 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
overflowisauto,scrollorhidden. - 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.
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.
.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.
.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.
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.
.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.
.section-title {
position: sticky;
top: 0;
background: white; /* hide what scrolls underneath */
}Try it yourself
- 1
Remove the containing block
In the first example, delete
position: relativefrom.stage. Predict where.absgoes and what the console prints for it. (It's now placed against the initial containing block, the top right of the whole preview, so itsleftchanges with the preview's width.) - 2
Stretch with insets
In the second example, replace the badge's
width/heightwithinset: 4pxand removetop: 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
Make sticky stop working
In the fourth example, add
overflow: hiddentosection. 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
`.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
Console
Nothing logged yet.Expected console output
rel left 20 top 10
next left 0 top 40
abs left 180 top 10First 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
Console
Nothing logged yet.Expected console output
containing block: outer | left 256 top 0
containing block: middle | left 236 top 20`.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
Console
Nothing logged yet.Expected console output
.f1 left 0 top 0
.f2 left 60 top 50At 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
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 0Break 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.
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.
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()).
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/lefton 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. Withleft,rightandwidthall set, the box is over-constrained andrightis ignored in left-to-right text. Withleft,rightset andwidth: auto, the width is computed from them. This is whyinset: 0stretches 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.offsetParentin the console gives the nearest positioned ancestor (roughly the containing block of an absolute child). It isnullforposition: fixedboxes and for elements withdisplay: none. Firefox DevTools also marks offsets on a static element as inactive and explains why.
Remember this
- 1
position: staticis the default. The box sits wherever normal flow puts it, and the offset propertiestop,right,bottomandleft(and their shorthandinset) are ignored. A box with any other value is called positioned, and the offsets now mean something.inset: 10pxis short for all four sides;inset: 0meanstop: 0; right: 0; bottom: 0; left: 0. - 2
position: relativekeeps the box in flow, in its original place, then shifts how it is drawn by the offsets.top: 10pxmoves it 10px down (away from the top),left: 20pxmoves it 20px right. Its original space stays reserved, so siblings don't move, and the shifted box can overlap them. The most common use ofrelativeisn't moving anything: it's making the box a containing block for absolutely positioned children. - 3
position: absolutetakes 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 whosepositionisn'tstatic). 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: 10pxputs the box 10px from that block's top and right edges. An absolute box withwidth: autoshrinks to fit its content instead of filling the width. - 4
If an absolute box has offsets of
autoon an axis, it stays at its static position: where it would have been in normal flow, but still taking no space. If it has bothleftandright(orinset: 0), it stretches between them, and with a fixedwidthplusmargin: autoit centres itself (Topic 6.5). Percentages intop/bottomare measured against the containing block's height, and inleft/rightagainst its width. - 5
position: fixedworks likeabsolute, 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 atransform,filter,perspective,backdrop-filter,contain: paintorwill-change: transform, that ancestor becomes the containing block instead, and the "fixed" box scrolls with it. - 6
position: stickyis a hybrid. The box stays in flow and scrolls normally until it reaches a threshold you set, such astop: 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,leftorright); without one it acts likerelative.
Explain it without notes
Explain the five values of position and whether each keeps the box in normal flow.
What is the containing block of an absolutely positioned element?
Why might a position: fixed element scroll with the page?
How does position: sticky work, and what are the common reasons it fails?
Why do developers put position: relative on a parent without any offsets?
Practice
A .photo card holds an <img> and a <span class="tag">New</span>. Put the tag 10px from the card's bottom-left corner.
Make an overlay that exactly covers its .hero parent with a half-transparent black layer.
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
- ↔
absolutegives 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
insetto stretch an overlay over its parent.I know which ancestor properties break
position: fixed.I can debug a sticky header that doesn't stick.