Front-End · 9 min read · Updated

CSS position Explained: static, relative, absolute, fixed and sticky

Understand the five CSS position values, containing blocks, z-index and stacking contexts – plus why sticky sometimes refuses to stick.

Css position: free world map pins photo
Photo via Rawpixel (CC0)

The CSS position property decides how an element is placed on the page and what top, right, bottom and left mean for it. It is behind many everyday patterns: a “New” badge in the corner of a card, a sticky table header, a full-screen overlay, a chat button that stays in the corner.

It also causes a lot of confusion, because the same top: 0 can mean five different things. This guide explains each value, the one idea that ties them together – the containing block – and the bugs you are most likely to hit.

Tip: CSS position is for placing individual elements. For page layout – columns, rows, card grids – use Flexbox or Grid. Our lesson on display and position covers the basics if you are new to this.

The five CSS position values at a glance

ValueStays in normal flow?Offsets are relative toTypical use
staticYesOffsets are ignoredThe default for every element
relativeYes (its original space is kept)Its own normal positionSmall nudges; a reference point for absolute children
absoluteNoThe nearest positioned ancestorBadges, icons inside inputs, tooltips, dropdown panels
fixedNoThe viewport (with exceptions)Floating buttons, cookie bars, modal overlays
stickyYesIts nearest scrolling ancestor, once a threshold is reachedSticky headers, table headings, sidebars

static: the default

Every element starts as position: static. It sits wherever normal flow puts it, and top, left and z-index have no effect. You rarely write static yourself, except to reset an element that a framework or media query has positioned.

relative: nudge it, keep its space

position: relative keeps the element in the flow, then shifts it by the offsets you give. The important part: the space it originally used stays reserved, so nothing around it moves. top: 10px pushes it 10px down from where it would have been.

In practice, you will use relative far more often without any offsets. Setting it on a parent turns that parent into the reference point for absolutely positioned children. This is the single most useful CSS position pattern.

absolute: place it inside a parent

position: absolute takes the element out of normal flow – other content behaves as if it does not exist – and places it relative to its containing block. For an absolute element, that is the padding box of the nearest ancestor whose position is anything other than static. If no ancestor is positioned, it uses the initial containing block, which is roughly the size of the first screen of the page.

.card {
  position: relative;   /* becomes the containing block */
}

.card .badge {
  position: absolute;
  top: 12px;
  right: 12px;          /* 12px from the card's top-right corner */
}

Without position: relative on .card, the badge would fly off to the corner of the page – the most common absolute-positioning bug of all.

Useful absolute tricks

  • Stretch to fill the parent: inset: 0 is shorthand for top: 0; right: 0; bottom: 0; left: 0. Great for image overlays.
  • Centre a box of known size: inset: 0; margin: auto; with a width and height set.
  • Size from the edges: with both left and right set and width: auto, the element stretches between them.
  • Logical properties: inset-inline-start means “left” in left-to-right languages and “right” in right-to-left ones such as Arabic or Urdu.

fixed: pinned to the screen

position: fixed also leaves normal flow, but it is placed relative to the viewport. When the user scrolls, it stays put. That is ideal for a WhatsApp chat button, a “back to top” link or a modal backdrop.

.help-button {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 10;
}

Watch out: A fixed element stops being fixed to the viewport if any ancestor has a transform, perspective or filter other than none, certain contain values, or will-change: transform. That ancestor becomes its containing block instead, so the element scrolls with it. If your fixed header suddenly scrolls away, look for a transform on a parent – often added by an animation library.

Fixed elements also cover content. Leave room for them – for example, add padding-bottom to the body for a fixed footer bar – and check they do not hide form fields when the mobile keyboard opens.

sticky: normal until it sticks

position: sticky is a hybrid. The element scrolls normally until it reaches the threshold you set, then it sticks, and it stops sticking when its parent scrolls out of view.

.section-title {
  position: sticky;
  top: 0;            /* required: the point where it sticks */
  background: white; /* so content does not show through */
}

Why sticky does not stick

  1. No threshold. You must set at least one of top, bottom, left or right. Without one, sticky behaves like relative.
  2. An ancestor has overflow set. Sticky sticks to the nearest ancestor with a scrolling mechanism – that includes overflow: hidden, auto or scroll. If a wrapper has overflow: hidden (often added to fix a horizontal scrollbar), sticky works inside that wrapper, not the page. overflow: clip does not create a scroll container, so it is a useful alternative for clipping.
  3. The parent is too short. A sticky element cannot leave its parent. If the parent is exactly as tall as the sticky child, there is no room to stick.
  4. Flex or grid stretching. In a flex row, a sticky sidebar stretches to full height by default, so it has nowhere to move. Add align-self: start.

Try all five in one demo

Scroll inside the preview. Watch the sticky bar, the badge on the card, the relative box that keeps its space, and the fixed button that never moves.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CSS position demo</title>
<style>
  body { font-family: system-ui, sans-serif; margin: 0; background: #f8fafc; }
  .sticky-bar { position: sticky; top: 0; background: #1e3a8a; color: #fff; padding: 10px 16px; z-index: 2; }
  main { padding: 16px; }
  .box { background: #e2e8f0; padding: 12px; margin: 8px 0; border-radius: 6px; }
  .relative { position: relative; top: 10px; left: 20px; background: #fde68a; }
  .card { position: relative; background: #fff; border: 1px solid #cbd5e1; border-radius: 8px; padding: 24px 16px 16px; margin: 24px 0; }
  .badge { position: absolute; top: -10px; right: 12px; background: #dc2626; color: #fff; font-size: 12px; padding: 2px 8px; border-radius: 99px; }
  .overlay-wrap { position: relative; height: 120px; border-radius: 8px; background: linear-gradient(135deg, #0ea5e9, #22c55e); overflow: hidden; }
  .overlay { position: absolute; inset: auto 0 0 0; background: rgb(0 0 0 / 0.55); color: #fff; padding: 8px 12px; }
  .fab { position: fixed; right: 16px; bottom: 16px; background: #16a34a; color: #fff; border: 0; border-radius: 99px; padding: 12px 18px; font: inherit; }
  .spacer { height: 600px; }
</style>
</head>
<body>
  <div class="sticky-bar">Sticky bar: scroll down, I stay at the top</div>
  <main>
    <div class="box">Static box (normal flow)</div>
    <div class="box relative">Relative box: moved 10px down and 20px right</div>
    <div class="box">Static box: notice the gap above is still reserved</div>
    <div class="card">
      <span class="badge">New</span>
      Card with <code>position: relative</code>. The badge is absolute inside it.
    </div>
    <div class="overlay-wrap">
      <div class="overlay">Absolute caption pinned to the bottom with inset</div>
    </div>
    <p id="info"></p>
    <div class="spacer"></div>
  </main>
  <button class="fab" type="button">Fixed button</button>
<script>
  const pos = s => getComputedStyle(document.querySelector(s)).position;
  document.getElementById('info').textContent =
    'Computed: bar=' + pos('.sticky-bar') + ', badge=' + pos('.badge') + ', button=' + pos('.fab');
</script>
</body>
</html>

z-index and stacking contexts

When positioned elements overlap, z-index decides which is on top. Two rules explain almost every “z-index is not working” question:

  1. z-index needs a positioned element (or a flex or grid item). On a plain static block, it does nothing.
  2. z-index only competes within the same stacking context. A stacking context is like a sealed layer. Children can never rise above something outside their parent’s layer, however large their z-index.

A new stacking context is created by, among other things: a positioned element with a z-index other than auto; any fixed or sticky element; opacity less than 1; and transform or filter other than none. So if a dropdown with z-index: 9999 still appears under the header, check whether its parent created a stacking context with a lower value.

Tip: Use a small, documented z-index scale instead of random large numbers – for example 1 for raised cards, 10 for sticky headers, 100 for dropdowns, 1000 for modals. Storing them as custom properties such as --z-modal keeps everyone consistent. See CSS Custom Properties as Design Tokens.

Accessibility and responsive checks

  • Positioning changes where things look, not their order in the HTML. Keyboard and screen reader users follow the source order, so keep it logical.
  • Do not hide off-screen content with left: -9999px if it should be invisible to everyone – use hidden or display: none. The off-screen trick is only for text meant for screen readers.
  • Test fixed and sticky elements on a small phone. A sticky header plus a fixed cookie bar can leave very little room for content, and content scrolled to with an anchor link can end up hidden under the header – scroll-margin-top fixes that.
  • On mobile, prefer modest sticky headers. Large ones eat screen space. See Responsive Design Breakpoints.

Quick decision guide

  • Need to place something in a corner of a component? Parent relative, child absolute.
  • Should it stay on screen while the page scrolls? fixed.
  • Should it scroll with its section, then stick? sticky with a top value.
  • Arranging many items in rows or columns? Not position at all – use Flexbox or Grid.
  • Bootstrap user? It has helper classes such as position-relative, position-absolute, sticky-top and fixed-bottom, covered in Bootstrap spacing and utilities.

CSS position questions are common in interviews, too – see HTML and CSS Interview Questions. If you can explain the containing block and stacking contexts clearly, you will stand out.

Frequently asked questions

What is the difference between relative and absolute position?

A relative element stays in normal flow and keeps its space; offsets shift it from where it would have been. An absolute element is removed from flow and placed relative to its nearest positioned ancestor.

Why is my absolute element positioned relative to the whole page?

None of its ancestors is positioned. Add position: relative to the parent you want as the reference point.

Why is position: sticky not working?

The usual causes are: no top (or other inset) value, an ancestor with overflow: hidden, auto or scroll, a parent that is no taller than the sticky element, or a flex item stretched to full height.

What is the difference between fixed and sticky?

Fixed is always placed relative to the viewport and leaves normal flow. Sticky stays in normal flow and only sticks while its parent is on screen.

Why does z-index not work on my element?

Either the element is not positioned (and is not a flex or grid item), or it sits inside a stacking context that is itself below the element it should cover. Check parents for z-index, opacity, transform or filter.

Further reading

Keep reading

All articles