Most of the time, elements land on the page exactly where the normal document flow puts them — one after another, top to bottom, left to right. That default is great, and you should lean on it. But every so often you need to break out of that flow: pin a banner to the top of the screen, drop a little badge in the corner of a card, or keep a sidebar heading visible as the user scrolls. That’s what the position property is for.
Positioning trips a lot of people up because the values sound similar but behave very differently, and because they all hinge on one idea — the positioning context — that nobody explains clearly. Once that clicks, the whole thing stops being mysterious. Let’s walk through all five values and the rules that tie them together.
The position property and its five values
Every element has a position, and it’s one of five values:
.element {
position: static; /* the default */
/* position: relative; */
/* position: absolute; */
/* position: fixed; */
/* position: sticky; */
}
The value you choose changes two things: whether the element stays in the normal flow, and what the top, right, bottom, and left offset properties do to it. Those four offsets are the steering wheel of positioning — but they only have an effect once you’ve moved away from static.
top/right/bottom/left do nothing on static elements
This is the number-one beginner confusion: you set top: 20px on an element, nothing happens, and you assume CSS is broken. It isn’t. The offset properties are simply ignored while position: static (the default). They only kick in once you switch to relative, absolute, fixed, or sticky. If your offsets aren’t working, check the position value first.
static — the default flow
position: static is what every element starts as. The element sits wherever the normal document flow places it, and top/left/etc. are ignored. You rarely write position: static yourself — it’s mostly useful for resetting an element back to normal after it inherited or was given another value.
.box {
position: static; /* sits in normal flow; offsets ignored */
}
There’s nothing more to it. Static is the calm baseline everything else departs from.
relative — nudge from where it already is
position: relative keeps the element in the normal flow but lets you offset it from its own original spot. The space it originally occupied is still reserved, so neighboring elements don’t move to fill the gap — the element just visually shifts.
.box {
position: relative;
top: 10px; /* move down 10px from where it was */
left: 20px; /* move right 20px from where it was */
}
Read those offsets carefully: top: 10px pushes the element down (away from the top), and left: 20px pushes it right (away from the left). The element moves relative to itself, leaving a ghost-shaped gap behind.
On its own, relative is only mildly useful for nudging things. Its real superpower is something else entirely: it creates a positioning context for absolutely-positioned children. Hold that thought — it’s the key to the next section.
absolute — pulled out of flow, placed against an ancestor
position: absolute is the big one. It removes the element from the normal flow entirely (neighbors close up as if it were never there) and positions it using the offsets — but relative to its nearest positioned ancestor, not to itself.
“Positioned ancestor” means the closest parent (or grandparent, etc.) that has a position other than static — usually relative. If no such ancestor exists, the element is positioned against the page itself (the initial containing block).
.card {
position: relative; /* becomes the positioning context */
}
.badge {
position: absolute;
top: 8px;
right: 8px; /* pin to the top-right corner of .card */
}
<div class="card">
<span class="badge">New</span>
<p>Card content goes here.</p>
</div>
Because .card is relative, the absolutely-positioned .badge measures its top and right from .card’s edges — landing it neatly in the top-right corner. This relative parent + absolute child pairing is one of the most useful patterns in all of CSS: badges, dropdown menus, tooltips, close buttons, image overlays. Learn it once and you’ll use it constantly.
The golden rule: relative parent, absolute child
When you want to place something precisely inside a box, give the box position: relative and the inner element position: absolute. The relative parent anchors the absolute child. Forget the relative on the parent and your “corner badge” will fly off to the corner of the whole page instead — that’s the classic bug, and now you know the fix.
If you set all four offsets at once on an absolute element, it stretches to fill its positioning context:
.overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0; /* covers the entire positioned ancestor */
}
That’s a tidy way to lay a semi-transparent overlay across a whole card or hero image.
fixed — pinned to the viewport
position: fixed also pulls the element out of the flow, but it pins it to the viewport — the visible browser window — rather than to any ancestor. The element then stays put even as the page scrolls.
.back-to-top {
position: fixed;
bottom: 24px;
right: 24px; /* always 24px from the bottom-right of the screen */
}
That’s exactly how a floating “back to top” button, a cookie banner, or a sticky header that never moves is built. Because it’s measured against the viewport, scrolling has no effect — the element is glued to that spot on the screen.
Fixed on mobile, and the transform gotcha
Two things to watch with fixed. First, on mobile, fixed bars can overlap content or fight with the on-screen keyboard, so test on a real device and leave room with padding. Second — and this surprises everyone — if any ancestor has a transform, filter, or perspective set, your fixed element becomes fixed relative to that ancestor instead of the viewport. If a fixed element mysteriously scrolls away, look for a transform on a parent.
sticky — relative until it sticks
position: sticky is the clever hybrid, and the newest of the bunch. The element behaves like relative (staying in flow) until it scrolls to a threshold you set, at which point it “sticks” and behaves like fixed — but only while its container is still on screen.
.section-heading {
position: sticky;
top: 0; /* sticks once it reaches the top of the viewport */
}
As you scroll, the heading scrolls normally until it hits the top edge, then it freezes there while the rest of that section scrolls past underneath. Once the section is gone, the heading scrolls away with it. This is how those “sticky” category headers in long lists, table headers, and section navigations work.
Two things are non-negotiable for sticky to work: you must specify a threshold (like top: 0), and the element needs a scrollable ancestor with room to scroll. A common silent failure is an ancestor with overflow: hidden, which quietly disables stickiness.
Quick comparison
Here’s the whole family side by side, which is the fastest way to keep them straight:
| Value | In normal flow? | Offsets measured from | Scrolls with page? |
|---|---|---|---|
static |
Yes | (offsets ignored) | Yes |
relative |
Yes (space kept) | Its own original position | Yes |
absolute |
No | Nearest positioned ancestor | Yes (with that ancestor) |
fixed |
No | The viewport | No (stays put) |
sticky |
Yes, then pins | Nearest scroll container | Until the threshold, then sticks |
If you remember just one row of context: absolute is measured against an ancestor, fixed against the viewport. That single distinction explains most of the “why is my element in the wrong place” headaches.
A practical example: a card with a corner badge
Let’s build the most common positioning pattern you’ll actually ship — a product card with a “Sale” badge pinned to its corner:
.product {
position: relative; /* the anchor */
width: 240px;
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;
}
.product .badge {
position: absolute;
top: 12px;
right: 12px;
background: #00b8e6;
color: #fff;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
}
<div class="product">
<span class="badge">Sale</span>
<h3>ACY Partner Indonesia Plan</h3>
<p>Everything you need to get started.</p>
</div>
The card is relative so it becomes the reference frame; the badge is absolute and sits 12px from the top-right corner of that card, no matter how tall the card grows. Resize, add text, change the layout — the badge stays glued to its corner. That resilience is the whole point of the relative/absolute pair.
Best practices and common mistakes
A few habits will save you a lot of debugging:
- Reach for flow and Flexbox/Grid first. Positioning is for overlaying and pinning, not for general layout. If you’re using
absoluteto build a whole page structure, step back — Flexbox and Grid are the right tools, and they’re far more robust. - Always set
position: relativeon the parent before placing anabsolutechild. This is the single most common positioning bug. - Don’t forget the offset on sticky.
position: stickywithout atop(orbottom/left/right) value does nothing at all. - Remember that
absoluteandfixedremove the element from flow. Surrounding content collapses as if the element vanished, which can cause overlaps. Reserve space with padding or margins on neighbors if needed. - Mind stacking order. When positioned elements overlap, which one wins is controlled by
z-index, which only applies to positioned elements — that’s a whole topic of its own, and a natural next step from here.
Positioning also assumes you’re comfortable with how an element’s size is built up from content, padding, border, and margin. If any of that feels shaky, it’s worth revisiting the CSS box model before going further — every offset you set interacts with it.
Wrapping up
The position property is how you take an element out of the ordinary flow and place it deliberately. Here’s the whole picture in a few lines:
staticis the default — normal flow, offsets ignored.relativenudges an element from its own spot and creates a positioning context for absolute children.absoluteremoves the element from flow and positions it against the nearest positioned ancestor (the classic relative parent, absolute child pattern).fixedpins the element to the viewport, so it ignores scrolling.stickyacts likerelativeuntil a scroll threshold, then pins likefixedwithin its container.
Get comfortable with the positioning context idea — the question “what is this measured against?” — and CSS positioning becomes one of the most predictable, useful tools in your kit. Next, when positioned elements start overlapping each other, you’ll want to control which one shows on top. That’s z-index, and it’s where we head next.