What you'll learn
Quick Answer
The CSS position property decides how an element is placed and whether top, right, bottom, and left actually move it. static is the default and ignores those offsets; relative nudges an element from its normal spot; absolute positions it against its nearest positioned ancestor; fixed pins it to the viewport; and sticky switches from relative to fixed once you scroll past a set point. Use z-index to control which positioned element stacks on top.
What Is the CSS Position Property?
The css position property tells the browser how to place an element on the page and whether the offset properties — top, right, bottom, and left — have any effect. It is one of the first things that feels confusing when you start CSS, but once you learn what each of the five values does, layout stops feeling like magic.
There are exactly five values you need to know:
static— the default, normal document flowrelative— nudged from its normal spotabsolute— placed against a positioned ancestorfixed— pinned to the browser windowsticky— relative until you scroll, then fixed
The offset properties only do something when an element is positioned (anything except static). On their own they set how far to move; the position value decides what they move relative to. Keep that one idea in your head and the rest falls into place. If you want a guided path from scratch, our free CSS course covers all of this with hands-on exercises.
static: The Default
Every element starts life as position: static. It sits in the normal flow of the page — block elements stack top to bottom, inline elements sit left to right. The important gotcha: offsets are ignored. Setting top or left on a static element does absolutely nothing.
.box {
position: static;
top: 50px; /* ignored */
left: 50px; /* ignored */
}You rarely type position: static yourself. It is mostly useful as a reset — putting an element back into normal flow after a media query or a more specific rule had set it to something else.
relative: Nudge Without Leaving the Flow
position: relative keeps the element in the normal flow but lets you shift it with the offset properties. The move is measured from where the element would have been. Crucially, the original space stays reserved — neighbouring elements do not slide over to fill the gap, so the element can overlap its siblings.
/* Before: normal position */
.note { position: static; }
/* After: nudged down and to the right */
.note {
position: relative;
top: 12px;
left: 12px;
}The .note element moves 12px down and 12px right, but the empty slot it left behind is still held open. The second (and more common) reason to use relative is subtle: it turns the element into a containing block for any absolute children. More on that next.
absolute: Positioned to Its Ancestor
position: absolute removes the element from the normal flow entirely — other elements act as if it is not there — and positions it using top/right/bottom/left. The question is: relative to what? The answer is the containing block: the nearest ancestor that is itself positioned (has a position of relative, absolute, fixed, or sticky). If no ancestor is positioned, it falls back to the initial containing block — roughly, the whole page.
This is the single biggest source of "why is my box in the corner of the entire page?" bugs. The fix is the classic pattern: mark the parent relative and the child absolute.
.card {
position: relative; /* becomes the containing block */
}
.badge {
position: absolute;
top: 8px;
right: 8px; /* 8px from the card's corner */
}Now .badge sits in the top-right corner of .card, not the top-right of the page. A "New" tag on a product card, a close button on a modal, and a dropdown menu are all built exactly this way.
fixed: Pinned to the Viewport
position: fixed also removes the element from the flow, but it is always positioned relative to the viewport — the visible browser window — so it stays put as you scroll. That makes it perfect for anything that should stay reachable: a cookie banner, a chat bubble, or a back-to-top button.
.back-to-top {
position: fixed;
bottom: 24px;
right: 24px;
width: 48px;
height: 48px;
border-radius: 50%;
z-index: 100;
}That button sits 24px from the bottom-right corner and never moves, no matter how far the user scrolls. One gotcha worth remembering: if any ancestor has a transform, filter, or perspective, then fixed is positioned relative to that ancestor instead of the viewport — a rare but genuinely maddening bug to track down.
sticky: The Best of Both
position: sticky is a hybrid. The element behaves like relative while it is in its normal place, then "sticks" like fixed once you scroll past a threshold you set with top (or bottom/left/right). It sticks only within its parent — once the parent scrolls out of view, the sticky element leaves with it.
.site-header {
position: sticky;
top: 0; /* sticks once it reaches the top edge */
z-index: 10;
background: #fff;
}An offset like top is required — without one, sticky has nothing to stick to and quietly does nothing. Two more gotchas that catch everyone: sticky fails silently if a parent element has overflow: hidden, scroll, or auto set, and the parent must be taller than the sticky element for the effect to be visible at all.
z-index and Stacking Order
When positioned elements overlap, z-index decides which one sits on top. A higher number wins. But there are two rules beginners trip over:
z-indexonly works on positioned elements — it is completely ignored onposition: static.- Without any
z-index, elements that come later in the HTML paint on top of earlier ones.
.modal { position: fixed; z-index: 1000; }
.overlay { position: fixed; z-index: 999; }
.tooltip { position: absolute; z-index: 10; }The tricky part is stacking contexts. When you set z-index on a positioned element (or use opacity, transform, and a few others), it creates a new stacking context, and its children's z-index values only compete inside that context. This is why a child with z-index: 9999 can still sit behind another element — its parent's stacking context is lower, and children can never escape their parent's box. When z-index "just won't work," a hidden stacking context is almost always the reason.
Which Position Should You Use?
Here is a quick cheat sheet. Match the job to the value:
| Value | In normal flow? | Offsets work? | Positioned relative to |
|---|---|---|---|
static | Yes | No | Nothing (default flow) |
relative | Yes | Yes | Its own normal position |
absolute | No | Yes | Nearest positioned ancestor |
fixed | No | Yes | The viewport |
sticky | Yes | Yes | Scroll position, within parent |
Our recommendation: reach for normal flow, Flexbox, and Grid first for overall layout — position is for the finishing touches, not the skeleton. Use relative to create containing blocks, absolute for small badges and icons inside a positioned parent, fixed for always-visible controls, and sticky for headers and section titles. Avoid building whole page layouts out of absolute positioning — it does not adapt to different screen sizes. Ready to practise? The free Priodemy CSS course has exercises for every value above.
Frequently Asked Questions
What is the difference between position: relative and position: absolute?
An element with position: relative stays in the normal document flow and is nudged from its own original spot, leaving its space reserved so nothing else moves. An element with position: absolute is removed from the flow entirely and positioned against its nearest positioned ancestor, so the surrounding elements behave as if it is not there.
Why is my z-index not working?
The two usual causes are that the element is still position: static (z-index only affects positioned elements), or that a parent has created a stacking context with its own z-index, opacity, or transform. A child can never escape its parent's stacking context, so raising the child's z-index will not lift it above elements outside that parent.
What is a containing block in CSS?
The containing block is the reference box an absolutely positioned element measures its offsets against. For position: absolute it is the nearest ancestor with a non-static position; for position: fixed it is usually the viewport. If no positioned ancestor exists, an absolute element falls back to the page, which is why it often lands in an unexpected corner.
When should I use sticky instead of fixed?
Use position: sticky when you want an element to scroll along normally and only pin once it reaches a set point, such as a table header or a section title. Use position: fixed when the element should stay in place from the very start and never move, like a back-to-top button or a chat widget.
Do I need position: relative on the parent for absolute positioning?
Not strictly, but you almost always want it. Without a positioned ancestor, an absolute element is placed relative to the whole page and usually lands somewhere unexpected. Adding position: relative to the parent makes it the containing block so the child positions neatly inside it.
