The CSS position property decides how an element is placed and what top, right, bottom and left are measured from. static is the default (offsets ignored), relative nudges an element from its normal spot, absolute places it inside its nearest positioned ancestor, fixed pins it to the screen, and sticky scrolls normally until it reaches an offset, then stays.
Tested with: Chromium 156 and WebKit 27.2 (Safari's engine) via Playwright, October 2026. We measured every example with getBoundingClientRect(); both engines gave identical results.
CSS Position Values at a Glance
| Value | In normal flow? | Offsets measured from | Scrolls with the page? | Typical use |
|---|---|---|---|---|
| static | Yes | Offsets ignored | Yes | Everything, by default |
| relative | Yes (space kept) | Its own normal position | Yes | Small nudges; parent for absolute children |
| absolute | No | Nearest positioned ancestor (containing block) | Yes | Badges, dropdowns, tooltips, overlays |
| fixed | No | The viewport (screen) | No | Headers, chat buttons, modals |
| sticky | Yes | Its scrolling container, once the offset is reached | Until it sticks | Section headings, table headers, sidebars |
position: static
.box {
position: static; /* the default */
top: 50px; /* ignored: offsets do nothing on static elements */
}Every element starts as static. It sits in normal document flow and top/left do nothing: in our test the static box stayed exactly where it was despite top: 50px.
position: relative
.box {
position: relative;
top: 20px; /* moves it 20px down from where it would be */
left: 10px; /* and 10px to the right */
}- The element moved 20px down and 10px right in our test.
- Its original space is kept: the next box stayed exactly where it would have been, 80px below the top (two 40px boxes), so a relative element can overlap its neighbors.
- Most of the time relative is used without any offset, just to make the element the containing block for absolute children.
position: absolute
.card {
position: relative; /* becomes the containing block */
}
.card .badge {
position: absolute;
top: 0;
right: 0; /* pinned to the card's top-right corner */
}An absolute element is taken out of the flow (other elements act as if it isn't there) and positioned against its containing block:
- The nearest ancestor with position other than static. In our test the badge sat at exactly 0px from the card's top and right edges.
- If there's none, the initial containing block (the first screen of the page). Our badge without a positioned parent went to the top-right of the page, not of its parent: the classic "my element jumped to the corner" bug.
- An ancestor with transform, filter or perspective also becomes the containing block, even if it isn't positioned. In our test the absolute child stuck to a transformed parent that had no position at all.
Percentages are relative to the containing block
.parent { position: relative; width: 400px; height: 100px; }
.dot { position: absolute; top: 50%; left: 50%; }
/* top = 50% of 100px = 50px, left = 50% of 400px = 200px */Measured result: 200px from the left and 50px from the top. Note that top: 50% moves the element's top edge to the middle, so it isn't centered yet; see Centering below.
An absolute element without a width shrinks to fit its content: a "Short" label measured 33px wide instead of filling the parent.
position: fixed
.site-header {
position: fixed;
top: 0;
inset-inline: 0; /* same as left: 0; right: 0 */
z-index: 100;
}
body {
padding-top: 64px; /* the header's height, so content isn't hidden under it */
}A fixed element is positioned against the viewport and stays in place when you scroll. After scrolling 400px, our fixed box was still at 0px from the top of the screen.
Why position: fixed is not fixed (transform, filter)
.page-wrapper {
transform: translateX(0); /* or filter, perspective, will-change: transform, contain: paint… */
}
.page-wrapper .chat-button {
position: fixed;
bottom: 16px;
right: 16px; /* no longer fixed to the screen: it scrolls with .page-wrapper */
}If any ancestor has transform, filter, perspective, contain: paint or will-change: transform, it becomes the containing block for fixed children (we confirmed all five in both engines; opacity and backdrop-filter did not break it). In our test, after scrolling 400px the normal fixed box stayed at 0, while the one inside a transform: translateX(0) wrapper had moved to 310px and the one inside filter: blur(0) to 430px: they scrolled with the page.
This often comes from animation libraries or page-transition wrappers that add a transform. Fix: move the fixed element outside that wrapper (in React, render it with a portal into document.body), or remove the transform once the animation ends.
position: sticky
.section-title {
position: sticky;
top: 0; /* required: sticks when it reaches the top of the scroll area */
background: white;
}A sticky element scrolls normally until it reaches the offset you set, then sticks until its parent scrolls out of view. Our results inside a scrolling box:
| Setup | Result |
|---|---|
| position: sticky; top: 0 | Sticks |
| No top (or bottom) set | Scrolls away |
| Parent has overflow: hidden | Scrolls away |
| Parent has overflow: clip | Sticks |
| Parent is exactly as tall as the sticky element | Scrolls away (no room to stick) |
| Inside a flex row with default align-items: stretch | Scrolls away (it's stretched to full height) |
| Same flex row with align-items: flex-start | Sticks |
Fixing sticky inside overflow and flex containers
/* Breaks sticky children: */
.wrapper { overflow: hidden; }
/* Clips the same way but keeps sticky working: */
.wrapper { overflow: clip; }.sidebar-layout {
display: flex;
align-items: flex-start; /* without this the sticky child stretches to full height */
}
.sidebar {
position: sticky;
top: 1rem;
}Sticky sticks to the nearest ancestor that scrolls (or could scroll). overflow: hidden makes the parent that ancestor even though it never scrolls, so nothing visible happens. overflow: clip clips without creating a scroll container, and it's supported in current Chrome, Safari and Firefox.
top, right, bottom, left and inset
- On relative they move the element from its normal spot; on absolute/fixed they set the distance from the containing block's edges; on sticky they set the point where it sticks.
- Setting both left and right (or top and bottom) on an absolute element stretches it between them.
- inset is the shorthand for all four:
.overlay {
position: absolute;
inset: 0; /* top: 0; right: 0; bottom: 0; left: 0 */
}Centering With position: absolute or fixed
.modal {
position: fixed; /* or absolute inside a positioned parent */
inset: 0;
margin: auto;
width: min(90vw, 480px);
height: fit-content;
}inset: 0 plus margin: auto centers an element that has a size. In our test a 100×50 box inside a 400×200 parent landed at exactly 150px left and 75px top. The older top: 50%; left: 50%; transform: translate(-50%, -50%) works too, but can blur text on some screens and the transform affects fixed children (see above).
z-index and Stacking Contexts
z-index decides which overlapping element is on top. Two rules trip people up:
1. z-index needs position, or a flex/grid parent
.row { display: flex; }
.row .card:hover {
z-index: 1; /* works without position, because .card is a flex item */
}In our test, z-index on two overlapping static blocks did nothing (the later one stayed on top), but on two flex items without any position it worked. The same is true for grid items.
2. A child can't escape its parent's stacking context
.header { position: relative; z-index: 1; }
.header .dropdown { position: absolute; z-index: 9999; } /* still under .banner */
.banner { position: relative; z-index: 2; }The dropdown's 9999 only counts inside .header. Since .header (1) is below .banner (2), everything in it is too. Our test confirmed it: the 9999 child stayed under a sibling with z-index 2.
Stacking contexts are created by more than z-index: opacity below 1, transform, filter, isolation: isolate and others. A child with z-index 9999 inside a parent with just opacity: 0.99 stayed under a sibling with z-index 1 in our test. Fix by raising the parent's z-index or moving the overlay out of it. Details: stacking context on MDN.
Anchor Positioning: Tooltips Without JavaScript
CSS anchor positioning lets an absolutely positioned element attach to another element anywhere on the page, which used to need JavaScript libraries:
.help-button {
anchor-name: --help;
}
.tooltip {
position: absolute;
position-anchor: --help;
top: anchor(bottom); /* tooltip's top = button's bottom */
left: anchor(center);
translate: -50% 8px; /* center it, leave an 8px gap */
position-try-fallbacks: flip-block; /* flip above if there's no room below */
}In both Chromium 156 and WebKit 27.2 the tooltip landed exactly 8px below the button and centered on it. Check current browser support before relying on it, and keep a simple fallback position for browsers without it. Reference: CSS anchor positioning on MDN.
relative vs absolute vs fixed vs sticky
| Question | relative | absolute | fixed | sticky |
|---|---|---|---|---|
| Leaves a gap where it was? | Yes | No | No | Yes |
| Moves when the page scrolls? | Yes | Yes | No | Until it sticks |
| Needs a positioned parent? | No | Usually (else the page) | No (but breaks under transform) | No (but a scrolling ancestor) |
| Works with top/left? | Yes | Yes | Yes | Needs at least one |
Common Problems and Fixes
| Problem | Cause | Fix |
|---|---|---|
| Absolute element jumps to the page corner | No positioned ancestor | Add position: relative to the intended parent |
| position: fixed scrolls with the page | Ancestor has transform/filter/perspective | Move it out (portal) or remove the transform |
| position: sticky does nothing | No top, or parent overflow: hidden, or flex stretch, or parent too short | Add top, use overflow: clip, align-items: flex-start |
| z-index has no effect | Static non-flex element, or a parent stacking context | Add position, or fix the parent's z-index |
| Fixed header covers content | Fixed elements leave the flow | padding-top on body; scroll-margin-top on anchors |
| Full-height mobile overlay is cut off | 100vh includes hidden browser bars | Use inset: 0 or 100dvh |
Full property reference: position on MDN. For layout itself (rows, columns, alignment), use Flexbox and Grid; positioning is for taking elements out of that layout. Our free CSS course covers both.
FAQs
What is the difference between position relative and absolute?
relative keeps the element in the flow and moves it from its normal spot, leaving its space behind. absolute removes it from the flow and places it inside the nearest positioned ancestor. They're used together: relative on the parent, absolute on the child.
What is the default position in CSS?
static. Static elements follow normal flow and ignore top, right, bottom, left and (outside flex and grid) z-index.
What's the difference between fixed and sticky?
fixed is always pinned to the screen and leaves the flow. sticky stays in the flow, scrolls normally, and only sticks while its parent is on screen.
Why is my position: sticky not working?
Check for a missing top, an ancestor with overflow: hidden or auto, a flex parent stretching the element, or a parent no taller than the element. The table above shows each case.
Why is my position: fixed element scrolling?
An ancestor has transform, filter, perspective or a similar property, which makes it the containing block. Move the element outside that ancestor.
Does z-index work without position?
On flex and grid items, yes. On normal block or inline elements, no: they need a position other than static.
About Muhaymin Bin Mehmood
Front-end Developer skilled in the MERN stack, experienced in web and mobile development. Proficient in React.js, Node.js, and Express.js, with a focus on client interactions, sales support, and high-performance applications.

