Blog / Coding tips

CSS Position Explained: Absolute vs Relative

Cover image for CSS Position Explained: Absolute vs Relative

CSS position decides how an element is placed. relative nudges an element from its normal spot but keeps its space in the layout; absolute takes it out of the layout and places it relative to the nearest positioned ancestor; fixed pins it to the viewport; and sticky acts normal until you scroll past a threshold, then sticks. The trick most beginners miss is that absolute needs a parent with position: relative. In this post I explain all five values with examples, and every behaviour I describe was checked by measuring elements in headless Chrome.

Positioning confused me for a long time. My absolutely positioned badges kept flying to the corner of the page, and my sticky headers refused to stick. Once I understood what each value is positioned relative to, it all clicked.

position: static (the default)

Every element starts as static. It sits in the normal flow of the page, and top, right, bottom, left and z-index have no effect. You rarely write position: static, except to undo another value.

position: relative

A relatively positioned element stays in the normal flow, and its original space is kept. The offsets move it visually from where it would have been:

.nudge {
  position: relative;
  top: 10px;   /* moves 10px down from where it would normally be */
  left: 20px;  /* and 20px to the right */
}

When I measured this, the nudged paragraph moved 20px to the right and 10px down, but the paragraph after it didn't move at all. The gap above the next paragraph shrank by exactly 10px, because the nudged one now overlaps the space below it. That's the key point: relative offsets don't push other elements around.

In practice I almost never use relative to nudge things. Its main job is the next section: being the anchor for absolutely positioned children.

position: absolute

An absolutely positioned element is removed from the normal flow. Other elements act as if it isn't there, and no space is left for it. Its offsets are measured from its containing block, which is the nearest ancestor with a position other than static. If there isn't one, it's positioned against the initial containing block, which is basically the first screen of the page.

Example: a "New" badge in the corner of a card

<article class="card">
  <span class="badge">New</span>
  <h2>CSS position explained</h2>
  <p>Absolute vs relative, fixed and sticky.</p>
</article>
.card {
  position: relative; /* becomes the containing block for .badge */
  padding: 1.5rem;
  border: 1px solid #2c3550;
  border-radius: 12px;
}

.badge {
  position: absolute;
  top: 12px;
  right: 12px;
  padding: 2px 10px;
  border-radius: 999px;
  background: #8b7cff;
  color: #fff;
}

With position: relative on the card, the badge sat 12px from the card's top and right edges, measured from inside the card's border. When I removed it, the badge jumped to 12px from the top of the page, far away from its card. That's the "flying badge" bug I kept hitting. The fix is always the same: give the parent position: relative.

Example: an overlay that covers its parent

.thumb {
  position: relative;
}

.thumb-overlay {
  position: absolute;
  inset: 0;              /* same as top: 0; right: 0; bottom: 0; left: 0; */
  display: grid;
  place-items: center;
  background: rgb(0 0 0 / 0.5);
}

inset is shorthand for all four offsets, and all major browsers support it. inset: 0 stretches the overlay over the whole parent, which is great for image captions, "play" buttons on video thumbnails and loading screens over a card.

Centring with absolute

.parent {
  position: relative;
  height: 300px;
}

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

top: 50% and left: 50% move the element's top-left corner to the middle, and translate(-50%, -50%) shifts it back by half its own size. These days I usually centre with Grid or Flexbox instead, but this is still handy for overlays.

Positioned layers like these are how many editors draw handles and guides on top of content. When I built my canvas design and animation editor, I took a different route and drew selection handles directly on the canvas, but for normal web pages, absolute positioning inside a relative parent is the standard tool.

position: fixed

A fixed element is also taken out of the flow, but it's positioned relative to the viewport, so it stays in place when you scroll. It's used for "back to top" buttons, chat widgets and cookie banners:

.back-to-top {
  position: fixed;
  right: 1.5rem;
  bottom: 1.5rem;
  z-index: 10;
}

One surprise: if any ancestor has a transform, filter or perspective, that ancestor becomes the containing block, and "fixed" stops being fixed to the viewport. In my test, a fixed button inside a parent with transform: translateX(0) sat 24px from the bottom of that parent instead of the screen, and it scrolled away with it.

position: sticky

Sticky is a mix of relative and fixed. The element stays in the normal flow until it reaches a threshold you set, then sticks there while its parent is still on screen:

.site-header {
  position: sticky;
  top: 0;            /* required: where it should stick */
  z-index: 10;
  background: #10131c;
}

thead th {
  position: sticky;
  top: 0;
  background: #171b27;
}

Sticky headers and sticky table headings are the two uses I see most. Remember three rules:

  • You must set a threshold, such as top: 0. Without it, sticky behaves like relative.
  • The element only sticks within its parent. When the parent scrolls out of view, the sticky element goes with it.
  • Give it a background, otherwise content scrolls visibly underneath it.

z-index and stacking contexts

z-index controls which element is on top when they overlap. It works on positioned elements (and on flex and grid items). Higher numbers come forward, but only within the same stacking context:

.modal-wrapper {
  position: relative;
  z-index: 1;          /* creates a stacking context */
}

.modal {
  position: fixed;
  z-index: 9999;       /* only competes inside .modal-wrapper's context */
}

.header {
  position: sticky;
  top: 0;
  z-index: 2;          /* sits above the whole .modal-wrapper, modal included */
}

I tested this: even with z-index: 9999, the modal appeared under the header with z-index: 2, because the modal's wrapper created a stacking context with z-index: 1. The modal can never rise above its wrapper's level. When a huge z-index doesn't work, look for a parent that creates a stacking context: a positioned element with a z-index, or an element with opacity below 1, a transform or a filter.

Common mistakes with CSS position

1. Forgetting position: relative on the parent

The number one absolute-positioning bug. If an absolute element ends up in the wrong place, check which ancestor is the nearest positioned one.

2. Sticky inside overflow: hidden

Sticky elements stick to their nearest scrolling ancestor. overflow: hidden, auto or scroll on a parent makes that parent the scrolling ancestor, so if it doesn't scroll itself, the element never sticks.

/* Sticky silently stops working here */
.layout {
  overflow: hidden; /* any overflow other than visible on an ancestor */
}

/* Fix: use overflow: clip instead, which doesn't create a scroll container */
.layout--fixed {
  overflow: clip;
}

In my test, a sticky header inside an overflow: hidden wrapper scrolled away 500px off the screen. With overflow: clip, which hides overflow without creating a scroll container, the header stayed at top: 0.

3. Using absolute for page layout

Absolute elements don't take up space, so text under them doesn't move and things overlap as soon as the content or screen size changes. Use Flexbox or Grid for layout, and absolute only for small overlays.

4. Fighting z-index wars

Adding z-index: 99999 rarely fixes a stacking problem. Find the stacking context instead, and keep a small scale, like 1 for raised content, 10 for headers and 100 for modals.

5. Fixed elements covering content

A fixed header doesn't take up space, so the top of your page hides under it. Add padding to the body, or use position: sticky, which keeps its space.

FAQ

What's the difference between absolute and relative positioning?

Relative keeps the element in the flow and moves it from its normal spot. Absolute removes it from the flow and positions it inside the nearest positioned ancestor. They're often used together: relative on the parent, absolute on the child.

What's the difference between fixed and sticky?

Fixed is always pinned to the viewport and takes no space in the layout. Sticky stays in the layout and only sticks while scrolling, within its parent.

Why isn't my position: sticky working?

Check that you've set top (or another offset), that no ancestor has overflow set to hidden, auto or scroll, and that the parent is taller than the sticky element so it has room to stick.

Does z-index work without position?

On normal block elements, no. It works on positioned elements and on children of flex and grid containers.

Conclusion

Ask "relative to what?" for every position value: relative to its own normal spot, absolute to the nearest positioned ancestor, fixed to the viewport, and sticky to its scrolling ancestor once a threshold is reached. Add position: relative to parents of absolute children, avoid overflow: hidden around sticky elements, and look for stacking contexts when z-index acts strangely.

The MDN page on the CSS position property has interactive examples of each value if you want to experiment.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes

Gemini Structured Output With Zod in Next.js
Coding tips

Gemini Structured Output With Zod in Next.js

Describe the JSON once in Zod, send z.toJSONSchema() to Gemini as responseJsonSchema, then validate the reply with safeParse and retry once with the errors.

October 2, 2026 · 9 min read

How to Add a Gemini API Key to Next.js and Vercel
Coding tips

How to Add a Gemini API Key to Next.js and Vercel

Step by step: a Gemini key from Google AI Studio in .env.local, a server-only helper, a Next.js route handler, Vercel environment variables and a leak test.

October 2, 2026 · 8 min read

Build an Offline Habit Tracker With localStorage
Coding tips

Build an Offline Habit Tracker With localStorage

A small offline habit tracker in plain HTML and JavaScript: habits saved as JSON in localStorage, streaks from local dates, a 7-day row and a JSON backup.

October 2, 2026 · 10 min read