Toolmingo
Guides7 min read

CSS Position: Complete Guide to static, relative, absolute, fixed, and sticky

Master every CSS position value — static, relative, absolute, fixed, sticky — plus z-index and stacking context. Includes a cheat sheet, real-world patterns, and the six mistakes that trip up every developer.

position is one of the most powerful — and most misunderstood — CSS properties. Choosing the wrong value results in elements floating off-screen, sticky headers that never stick, or z-index battles that never end. This guide explains every value clearly, shows when to reach for each one, and reveals the stacking-context rules that control which element sits on top.


Quick reference

Value In normal flow? Positioned? Offset relative to…
static Yes No (offsets ignored)
relative Yes Yes Its own normal-flow position
absolute No Yes Nearest positioned ancestor
fixed No Yes Viewport
sticky Yes Yes Scroll container (hybrid)

"Positioned" means the element responds to top, right, bottom, left, and participates in stacking (z-index works on it).


position: static — the default

Every element starts as static. It sits in the normal document flow, and offset properties (top, left, etc.) have no effect.

.box {
  position: static; /* default — usually not written explicitly */
}

You only write static when you need to reset a positioned element back to its default.


position: relative — nudge without leaving flow

A relatively positioned element stays in the normal flow (it still occupies its original space), but you can shift it visually with top/right/bottom/left.

.nudged {
  position: relative;
  top: 10px;   /* shifts DOWN 10px from its normal position */
  left: 20px;  /* shifts RIGHT 20px */
}

The space it originally occupied is preserved — neighbouring elements are not affected.

More importantly, relative creates a positioning context for any absolute children inside it. This is its most common use.


position: absolute — escape the flow, anchor to a parent

An absolutely positioned element is removed from normal flow — it no longer occupies space — and is placed relative to its nearest positioned ancestor (any element with position other than static).

.parent {
  position: relative; /* establishes context */
}

.badge {
  position: absolute;
  top: 8px;
  right: 8px;
}

If no positioned ancestor exists, the element is positioned relative to the initial containing block (roughly the <html> element), which is almost never what you want.

Common patterns with absolute

Centre inside a container:

.container {
  position: relative;
  width: 300px;
  height: 200px;
}

.centered {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

Cover the entire parent:

.overlay {
  position: absolute;
  inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0 */
}

Bottom-align a caption:

.caption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
}

position: fixed — locked to the viewport

A fixed element behaves like absolute but anchors to the viewport, not any ancestor. It does not scroll with the page.

.navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
}

.back-to-top {
  position: fixed;
  bottom: 24px;
  right: 24px;
}

Watch out: A transform, filter, perspective, or will-change on any ancestor breaks fixed positioning — the element becomes fixed to that ancestor instead of the viewport. This is a common source of "my fixed element scrolls with the content" bugs.


position: sticky — scroll-triggered pinning

A sticky element behaves like relative until the scroll position reaches a threshold, then it pins like fixed — all within its scroll container and parent.

.section-header {
  position: sticky;
  top: 0;           /* sticks when it reaches the top of the scroll container */
  background: white;
  z-index: 10;
}

How sticky really works

Three things must be true for sticky to work:

  1. Specify a threshold — at least one of top/bottom/left/right must be set.
  2. The parent must be taller than the sticky element. When the parent ends, the sticky element scrolls away with it.
  3. No overflow: hidden or overflow: auto on any ancestor — these clip the sticky behaviour.
/* Table header that sticks while you scroll the table */
thead th {
  position: sticky;
  top: 0;
  background: #f5f5f5;
}

z-index and stacking context

z-index controls the paint order of positioned elements. Higher values appear on top.

.modal-backdrop { z-index: 200; }
.modal          { z-index: 201; }
.tooltip        { z-index: 300; }

What creates a stacking context?

A stacking context is an isolated layer. z-index values inside it only compete with siblings in the same context — they cannot "escape" to the outer context.

Stacking context triggers (most common):

Property Value
position + z-index any value except auto
opacity < 1
transform any value except none
filter any value except none
isolation isolate
will-change most values

Classic z-index bug: A modal with z-index: 9999 appears behind a sidebar because the sidebar's parent has transform: translateZ(0) — creating a new stacking context. The modal is trapped inside a context with a lower order.

/* Force a new, isolated stacking context */
.modal-root {
  isolation: isolate;
}

All five values — real-world layout

<div class="page">
  <nav class="navbar">Fixed navbar</nav>
  <section class="section">
    <h2 class="sticky-heading">Sticky heading</h2>
    <div class="card">
      <span class="badge">New</span>  <!-- absolute inside relative -->
      Card content
    </div>
  </section>
  <button class="fab">↑</button>  <!-- fixed -->
</div>
.navbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 60px;
  z-index: 100;
}

.section { padding-top: 60px; } /* offset for fixed navbar */

.sticky-heading {
  position: sticky;
  top: 60px; /* below the navbar */
  background: white;
  z-index: 10;
}

.card { position: relative; }  /* context for .badge */

.badge {
  position: absolute;
  top: 8px; right: 8px;
}

.fab {
  position: fixed;
  bottom: 24px; right: 24px;
  z-index: 50;
}

6 common mistakes

1. Forgetting position: relative on the parent

You set position: absolute on a child and expect it to anchor to its parent, but no ancestor is positioned — the element flies off to the <html> corner.

Fix: Always add position: relative (or any non-static value) to the intended anchor parent.

2. z-index on a static element does nothing

z-index has no effect unless the element is positioned (relative, absolute, fixed, sticky) or is a flex/grid item.

/* Broken */
.box { position: static; z-index: 999; } /* ignored */

/* Fixed */
.box { position: relative; z-index: 999; }

3. position: sticky not sticking

The most common culprits:

  • Missing threshold (top, bottom, etc. not set)
  • An ancestor has overflow: hidden or overflow: auto
  • The parent is the same height as the sticky element (nothing to stick within)

4. transform breaking fixed positioning

Adding any transform to a parent (including transform: translateZ(0) used as a GPU-compositing hack) turns it into a containing block for fixed descendants — they no longer stay fixed to the viewport.

5. Body padding/margin when using fixed at the top

A position: fixed navbar doesn't push content down. The content scrolls under it.

Fix: Add padding-top (equal to the navbar height) to <body> or the first content element.

6. Stacking context trapping z-index

A child with z-index: 9999 inside a parent with opacity: 0.99 will never appear above a sibling element that is outside that parent, regardless of its z-index value.

Fix: Use isolation: isolate deliberately, restructure the DOM, or move the element to the document root.


FAQ

Q: What's the difference between absolute and fixed?
Both are removed from flow. absolute is positioned relative to the nearest positioned ancestor; fixed is always relative to the viewport and doesn't move when the page scrolls.

Q: Can I use top/left on a static element?
No. Offset properties are ignored for static elements. Switch to relative if you just want to nudge something.

Q: Why does position: sticky work in some browsers but not others?
Old Safari required the -webkit-sticky prefix. Modern Safari (12+) supports the unprefixed version. More likely the issue is an overflow on an ancestor or a missing threshold value.

Q: What's inset in CSS?
inset is a shorthand for top, right, bottom, left — just like margin is shorthand for four sides. inset: 0 is equivalent to top:0; right:0; bottom:0; left:0, and is great for position: absolute overlays.

Q: How do I stack multiple sticky elements (e.g. two sticky headers)?
Give each a different top value matching the cumulative height above:

.primary-nav   { position: sticky; top: 0;   height: 60px; z-index: 20; }
.secondary-nav { position: sticky; top: 60px; height: 48px; z-index: 19; }

Q: Is position: absolute bad for accessibility?
Only if it visually reorders content from the DOM order. Screen readers follow DOM order, not visual order. Use absolute positioning for decorative elements and overlays; never for reordering meaningful content (use CSS order on flex/grid instead, and even then be careful).

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools