Home/Learn/HTML, CSS & Responsive/Positioning — relative, absolute, fixed and sticky

Positioning — relative, absolute, fixed and sticky

Intermediate
Layout

Positioning takes an element out of the normal flow, or anchors it while scrolling. The recurring question is always the same: positioned relative to what?

Overview

Four position values, and the one that trips everyone is absolute — because "absolute to what" has a precise answer: the nearest ancestor that is itself positioned. Forget to set position: relative on that ancestor and your dropdown anchors to the page instead of the button. Sticky is the newest and most useful of the four, and it fails silently in three specific situations that are worth memorising because the CSS looks perfectly correct when it does.

The Four Values

What each does to the flow, and what it is measured against.

inset: 0, and the positioned ancestor
/* static — the default. top/left/z-index do nothing. */

/* relative — stays in the flow and reserves its original space,
   but is visually nudged. Its main job is to be a positioning
   ancestor for something absolute inside it. */
.wrapper { position: relative }

/* absolute — REMOVED from the flow, positioned against the nearest
   positioned ancestor (or the page if there is none). */
.badge { position: absolute; top: -.5rem; inset-inline-end: -.5rem }

/* fixed — removed from the flow, positioned against the VIEWPORT,
   so it does not scroll. */
.fab { position: fixed; inset-block-end: 1rem; inset-inline-end: 1rem }

/* sticky — in the flow until it hits its threshold, then it stays. */
thead th { position: sticky; top: 0 }

/* inset is the shorthand for all four offsets */
.overlay { position: absolute; inset: 0 }        /* fill the parent */
.modal { position: fixed; inset: 0; display: grid; place-items: center }

/* Centring something absolute */
.center { position: absolute; inset: 0; margin: auto; width: 200px;
          height: 100px }

Why Sticky Silently Fails

Three causes. The CSS looks right in all of them.

Threshold, overflow ancestor, parent height
/* 1. No threshold. Sticky needs at least one of top/bottom/
      left/right — without it there is nothing to stick to. */
.nav { position: sticky }            /* does nothing */
.nav { position: sticky; top: 0 }    /* works */

/* 2. An ancestor has overflow: hidden, auto or scroll. Sticky
      positions within its nearest scrolling ancestor, and that
      container becomes it. This is the most common cause, and the
      overflow is often several levels up and set for an unrelated
      reason. */
.some-wrapper { overflow: hidden }   /* breaks sticky inside it */

/* 3. The parent is not tall enough. A sticky element can only move
      within its PARENT's box, so if the parent is exactly as tall
      as the element, there is nowhere to stick. */
.sidebar-parent { height: auto }     /* not: align-items: stretch
                                        making it exactly as tall  */

/* Debugging order: check for a threshold, then walk up the tree in
   DevTools looking for overflow, then check the parent's height. */

/* A common combination: a sticky header that offsets anchor links */
:target { scroll-margin-block-start: 5rem }
html { scroll-padding-block-start: 5rem }

Anchoring a Popover

The pattern behind every dropdown, tooltip and menu — and the modern alternatives.

relative + absolute, and the popover top layer
.dropdown { position: relative }            /* the anchor */
.dropdown__menu {
  position: absolute;
  inset-block-start: 100%;                 /* directly below */
  inset-inline-start: 0;
  min-width: 100%;
}

/* The problem this hits: the menu is clipped by any ancestor with
   overflow: hidden, and it cannot escape a low z-index stacking
   context. That is exactly why component libraries render menus in
   a portal at the end of the body. */

/* The platform is fixing this. The popover attribute renders in the
   top layer, above everything, with no z-index and no portal: */
<button popovertarget="menu">Options</button>
<div id="menu" popover>…</div>

/* And CSS anchor positioning ties it back to its trigger: */
.trigger { anchor-name: --trigger }
.menu { position: absolute; position-anchor: --trigger;
        top: anchor(bottom); left: anchor(left) }

/* Check support before relying on these; the portal pattern remains
   the safe default in 2026. */

Key Points to Remember

  • 1An absolutely positioned element is measured against the nearest positioned ancestor, or the page if there is none
  • 2inset: 0 fills the containing block and is the shorthand for all four offsets
  • 3Sticky needs a threshold, an ancestor without overflow hidden/auto, and a parent taller than itself
  • 4A sticky header needs scroll-padding so anchor links do not land underneath it
  • 5Absolutely positioned menus are clipped by overflow and trapped by stacking contexts, which is why libraries use portals or the popover top layer

Interview Questions

Sign in to ask Aria
1

What is position: absolute positioned relative to?

Easy
2

Name three reasons position: sticky might not work.

Hard
3

Why do component libraries render dropdown menus in a portal?

Hard

Ask Aria about Positioning — relative, absolute, fixed and sticky

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…