Positioning

📌 Chapter 7 — Positioning

Every element on a webpage occupies a position determined by the normal document flow — block elements stack top to bottom, inline elements flow left to right within their line. The position property lets you step outside that flow. You can nudge an element away from where it would normally sit, pin something to a corner of its container, lock an element to the browser window, or make a header that sticks to the top of the screen as the user scrolls. Understanding positioning — especially the relationship between relative and absolute — is one of the most important milestones in CSS.

1 — The Offset Properties

The position property works alongside four offset propertiestop, right, bottom, and left — that specify how far to move the element from a reference point. Which reference point they use depends on the position value.

🎨 CSS — The offset properties
.box { position: relative; /* or absolute, fixed, sticky */ top: 20px; /* push DOWN from the top edge */ right: 10px; /* push LEFT from the right edge */ bottom: 0; /* push UP from the bottom edge */ left: 30px; /* push RIGHT from the left edge */ } /* On position: static (the default), these offsets are IGNORED */
Offset direction is the opposite of what you might expect. top: 20px means "20px from the top edge" — the element moves down. right: 10px means "10px from the right edge" — the element moves left. Think of it as "how far is this element from this edge?", not "move in this direction".

2 — position: static (the default)

Every element starts with position: static. In this state:

  • The element sits exactly where the normal document flow places it.
  • The offset properties (top, right, bottom, left) are completely ignored.
  • A static element does not create a positioning context for absolutely-positioned children (more on this in section 4).
🎨 CSS — Static position (default, rarely written explicitly)
.box { position: static; /* default — no need to write this explicitly */ top: 50px; /* IGNORED — has no effect when position is static */ }
You will almost never write position: static yourself — it is the default. You might use it to reset a positioned element back to normal flow, e.g. on a smaller screen.

3 — position: relative

A relatively-positioned element is nudged from its own normal-flow position. Two key things to understand:

  • The original space is preserved. The element's "ghost" remains where it would have been — surrounding elements behave as if the element never moved.
  • It creates a positioning context. Any absolutely-positioned children will use this element as their reference point.
Live demo — the blue box is shifted 30px right and 12px up from its natural position; the gap it leaves remains
Box above (static)
← ghost — the space Box A still occupies in the flow
Box A — position: relative; left: 30px; top: -12px
Box below (static) — not pushed up, the ghost holds the space
🎨 CSS — position: relative
.nudged { position: relative; top: -12px; /* move UP 12px from natural position */ left: 30px; /* move RIGHT 30px from natural position */ /* surrounding elements are unaffected — the space is still reserved */ } /* Most common use: creating a positioning context for child elements */ .card { position: relative; /* no offsets — just establishes a context */ } .card .badge { position: absolute; /* positioned relative to .card, not the page */ top: 8px; right: 8px; }
💡 Relative is the anchor; absolute is the cargo. The most common use of position: relative is not to move the element at all — it is to create a positioning context so that an absolutely-positioned child has a well-defined parent to attach to.

4 — position: absolute

An absolutely-positioned element is removed from the normal document flow entirely — it leaves no ghost and surrounding elements collapse to fill the gap it left. Its offsets are measured from its containing block: the nearest ancestor that has a position other than static.

Live demo — the purple badge is absolutely positioned inside the green card container
Box above (static — not affected by the absolute child)
position: relative (the containing block)
Some card content here…
position: absolute
top: 10px; right: 10px
Box below — jumps up because the absolute child left no ghost

The containing block

When you apply position: absolute, CSS walks up the DOM tree looking for the first ancestor that is not position: static. That ancestor becomes the containing block — the coordinate system the offsets refer to. If no such ancestor exists, the element is positioned relative to the initial containing block (essentially the <html> element, which behaves like the page).

Diagram — which element does the absolute child attach to?
Page / html (fallback if no positioned ancestor)
✓ position: relative → this IS the containing block
Card content
absolute child
anchors here →
position: static (default) → NOT a containing block
Absolutely-positioned children look past this element
🎨 CSS — position: absolute in practice
/* Notification badge on an icon */ .icon-wrapper { position: relative; /* creates the containing block */ display: inline-block; } .badge { position: absolute; top: -6px; /* poke above the icon */ right: -6px; /* poke to the right of the icon */ background: #f08080; border-radius: 50%; width: 18px; height: 18px; font-size: 0.65rem; display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; } /* Full-cover overlay */ .overlay { position: absolute; inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0; */ background: rgba(0,0,0,0.5); }
inset: 0 is a shorthand for top: 0; right: 0; bottom: 0; left: 0 — it pins all four edges to the containing block, creating a full-cover layer. Supported in all modern browsers.
⚠️ Forgetting the containing block is the #1 positioning mistake. If your absolutely-positioned element appears in the wrong place, the first thing to check is whether the intended parent has position: relative (or any other non-static position). Without it, the absolute element will escape to the next positioned ancestor — often the entire page.

5 — position: fixed

A fixed element is removed from the document flow (like absolute) but its offsets are measured from the viewport — the visible browser window — not from any ancestor. This means the element stays in the same place on screen even as the user scrolls.

  • Common uses: sticky navigation bars, floating action buttons, cookie consent banners, chat widgets, back-to-top buttons.
  • Fixed elements always create their own stacking context — they sit above the page content and do not scroll with it.
  • Other content can be hidden beneath a fixed element unless you add padding/margin to account for its height.
What a fixed element looks like (this is a static representation — in a real page it would stay put as you scroll)
🔒 position: fixed — this would stay locked to the viewport corner as the page scrolls
🎨 CSS — position: fixed common patterns
/* Fixed top navigation bar */ .top-nav { position: fixed; top: 0; left: 0; right: 0; /* stretch to full viewport width */ height: 60px; background:#12141e; z-index: 100; /* sit above page content */ } /* Compensate for the nav height on the body */ body { padding-top: 60px; /* prevents nav from covering content */ } /* Floating action button — bottom-right corner */ .fab { position: fixed; bottom: 24px; right: 24px; width: 52px; height: 52px; border-radius:50%; background: #4f8ef7; z-index: 200; }

6 — position: sticky

Sticky is a hybrid between relative and fixed. A sticky element behaves like a normal in-flow element until its scroll position reaches a threshold you specify with an offset — at that point it sticks to the viewport edge and stays visible as the user scrolls, until its containing block scrolls off the screen.

  • You must set at least one offset (top, bottom, left, or right) for sticky to work — without an offset, the element behaves like relative.
  • The sticky element is constrained to its parent container — it cannot stick beyond the bounds of its containing element, which is why table headers with sticky work so well.
  • Common uses: table headers, section headings in long lists, sub-navigation.
Live demo — scroll inside the box; the yellow header sticks to the top
Item 1 — scroll down to see the header stick
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8 — the header reappears when this section scrolls back into view
🎨 CSS — position: sticky
/* Stick to the top when scrolling */ .section-header { position: sticky; top: 0; /* stick when it reaches the top edge of the viewport */ background: #1a1d27; /* give it a background — it sits over content */ z-index: 10; padding: 12px 0; } /* Sticky table header */ thead th { position: sticky; top: 0; background: #1e2235; z-index: 1; } /* Stick to bottom (e.g. mobile footer bar) */ .footer-bar { position: sticky; bottom: 0; }
The parent element must have enough height for the sticky effect to be visible — if the parent is shorter than the viewport, the element will never reach the threshold and appears to behave like relative.
⚠️ Common sticky failure: overflow: hidden on a parent. If any ancestor has overflow: hidden, overflow: auto, or overflow: scroll set, sticky positioning will stop working — the element will just scroll away normally. This is one of the most frustrating bugs to diagnose in CSS. Check your ancestor chain if sticky suddenly stops working.

7 — z-index and Stacking Order

When positioned elements overlap, CSS needs to decide which one appears on top. By default, elements that appear later in the HTML sit above elements that appear earlier. The z-index property lets you override this order explicitly.

  • Higher values sit in front; lower values sit behind.
  • z-index only works on positioned elements (relative, absolute, fixed, sticky). It has no effect on static elements.
  • Values can be negative — a negative z-index places an element behind its containing block.
  • Each positioned element can create its own stacking context, which means z-index values are compared within the same context, not globally across the whole page.
Live demo — three overlapping absolute boxes; the green (z-index: 3) is on top
z-index: 1
(behind)
z-index: 2
(middle)
z-index: 3
(on top)
🎨 CSS — z-index and stacking
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 1000; /* above navigation (z:100), below toasts (z:2000) */ } .modal-panel { position: fixed; top: 50%; left: 50%; transform:translate(-50%, -50%); /* centres the element on screen */ z-index: 1001; /* above the overlay */ } /* Negative z-index — behind the parent */ .card { position: relative; } .card::after { content: ''; position: absolute; inset: 6px -6px -6px 6px; /* shadow layer behind card */ background:#0a0c14; border-radius:inherit; z-index: -1; /* behind the card face */ }
A common convention is to manage z-index values in a scale: 10 (standard UI), 100 (sticky nav), 500 (dropdown menus), 1000 (modals), 2000 (toasts/notifications). This avoids the creeping "z-index: 9999" problem.

8 — Centering with absolute + transform

One of the most frequently used positioning tricks is centring an element on screen — inside a container or the full viewport. Before Flexbox, this was the reliable way to do it and it still appears in modal dialogs and overlays today.

🎨 CSS — The absolute centering pattern
.centered-box { position: absolute; top: 50%; /* move the TOP EDGE to the vertical centre */ left: 50%; /* move the LEFT EDGE to the horizontal centre */ transform: translate(-50%, -50%); /* ↑ shift the element back by half its own width and height so it is truly centred, not just its top-left corner */ } /* Why this works: top: 50% + left: 50% → places the top-left corner at the centre point translate(-50%, -50%) → nudges the element left by 50% of its OWN width and up by 50% of its OWN height Result: the element's centre is at the containing block's centre */
You will also see this pattern with position: fixed to centre an element on the viewport — perfect for modal dialogs.

9 — Quick Reference

Value In flow? Offsets from Typical use
static (default) Yes — (offsets ignored) Normal document flow; no positioning
relative Yes (ghost preserved) Its own natural position Small nudges; creating a positioning context for absolute children
absolute No (no ghost) Nearest positioned ancestor Badges, overlays, tooltips, decorative elements
fixed No (no ghost) The viewport Persistent navbars, cookie banners, floating buttons, modals
sticky Yes (until threshold) Viewport edge (after threshold); own position (before) Sticky table headers, section headings, sub-navigation
PropertyNotes
z-indexControls stacking order. Only works on positioned elements (non-static). Higher = in front. Compared within a stacking context.
insetShorthand for top + right + bottom + left. inset: 0 fills the containing block. Uses the same 1/2/3/4 clock shorthand as margin.
transform: translate()Often used alongside positioning. Moves the element by a percentage of its own dimensions rather than the parent's — useful for true centering.

✏️ Exercises

These exercises build a mental model for each positioning context. Inspect each result in DevTools — the Computed panel shows the actual position value and the Layout panel can highlight the containing block.

Exercise 1
Create a product card using position: relative as the container and position: absolute for a "New" badge in the top-right corner. The card should contain an image placeholder, a product title, and a price. The badge should sit partially outside the card's top-right corner (use negative top and right values). Make sure the absolute badge only positions itself relative to the card, not the whole page.
Hint: set position: relative on the card, and position: absolute; top: -10px; right: -10px on the badge. Set overflow: visible on the card if the badge is being clipped.
HTML
<div class="card"> <span class="badge">New</span> <div class="img-placeholder"></div> <h3>Wireless Headphones</h3> <p class="price">£89.99</p> </div>
CSS
.card { position: relative; /* creates the containing block */ width: 220px; border: 1px solid #2a2d3a; border-radius:8px; padding: 14px; background: #1a1d27; font-family: system-ui, sans-serif; } .badge { position: absolute; top: -10px; right: -10px; background: #4f8ef7; color: #fff; font-size: 0.72rem; font-weight: 700; padding: 3px 9px; border-radius:999px; text-transform:uppercase; letter-spacing:0.07em; } .img-placeholder { height: 120px; background: #12141e; border-radius:4px; margin-bottom:10px; }
Exercise 2
Build a fixed navigation bar. It should span the full width of the viewport, be 56px tall, and remain visible as the user scrolls down the page. Add enough body content (several paragraphs or placeholder divs) so you can actually scroll and observe the fixed bar staying in place. Add padding-top: 56px to the <body> so the fixed bar does not cover the first line of content. Give the nav a dark background and a subtle bottom border.
Hint: position: fixed; top: 0; left: 0; right: 0; pins the navbar to the top of the viewport. Set z-index: 100 so it sits above any page content it overlaps.
CSS
.fixed-nav { position: fixed; top: 0; left: 0; right: 0; height: 56px; background: #12141e; border-bottom:1px solid #2a2d3a; display: flex; align-items: center; padding: 0 20px; z-index: 100; font-family: system-ui, sans-serif; font-weight: 700; color: #e2e4ec; } body { padding-top: 56px; /* prevent nav covering the first content */ }
Exercise 3
Create a long page with multiple sections (e.g. "Chapter 1", "Chapter 2", "Chapter 3"), each at least 300px tall. Give each section heading position: sticky; top: 0 so that as the user scrolls through the page, the current chapter heading sticks to the top — and is then replaced by the next chapter's heading as it scrolls into view. Give each heading a solid background so it covers the content scrolling beneath it.
Hint: each heading only sticks within its own parent section — as the section scrolls off, the heading goes with it, and the next one takes over. This is how sticky naturally creates the "changing header" effect without any JavaScript.
HTML
<section> <h2 class="sticky-heading">Chapter 1 — Introduction</h2> <p>Lots of content…</p><p>More content…</p> </section> <section> <h2 class="sticky-heading">Chapter 2 — The Box Model</h2> <p>Lots of content…</p> </section> <section> <h2 class="sticky-heading">Chapter 3 — Positioning</h2> <p>Lots of content…</p> </section>
CSS
section { min-height: 350px; /* must be tall enough for sticky to engage */ padding: 0 20px 40px; } .sticky-heading { position: sticky; top: 0; background: #12141e; padding: 14px 0; margin: 0; border-bottom:2px solid #4f8ef7; z-index: 10; font-family:system-ui, sans-serif; color: #fff; }
Exercise 4
Build a simple modal dialog using position: fixed. The page should have a "Open modal" button. Clicking it adds a class that makes both a full-screen overlay and a centred modal panel visible. The modal should contain a heading, a short paragraph, and a "Close" button. Clicking the close button removes the class, hiding both. Use the top: 50%; left: 50%; transform: translate(-50%, -50%) technique to centre the panel on screen.
Hint: put both the overlay and the panel inside a wrapper div. Toggle display: none / display: block on the wrapper, and use position: fixed; inset: 0 for the overlay behind the panel. Set z-index: 1000 on the overlay and z-index: 1001 on the panel.
HTML
<button id="open-modal">Open modal</button> <div id="modal-wrapper" class="modal-wrapper is-hidden"> <div class="modal-overlay" id="close-overlay"></div> <div class="modal-panel"> <h2>Confirm action</h2> <p>Are you sure you want to continue?</p> <button id="close-modal">Close</button> </div> </div>
CSS
.is-hidden { display: none; } .modal-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.65); z-index: 1000; } .modal-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1a1d27; border: 1px solid #2a2d3a; border-radius:8px; padding: 28px 32px; min-width: 300px; z-index: 1001; font-family:system-ui, sans-serif; color: #e2e4ec; }
JavaScript
const wrapper = document.getElementById('modal-wrapper'); document.getElementById('open-modal').addEventListener('click', () => { wrapper.classList.remove('is-hidden'); }); ['close-modal', 'close-overlay'].forEach(id => { document.getElementById(id).addEventListener('click', () => { wrapper.classList.add('is-hidden'); }); });

Clicking the overlay dismisses the modal — a standard UX convention. Note that clicking anywhere outside the panel (on the overlay) also calls the same close handler, because the overlay covers the entire viewport behind the panel.