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 properties — top, 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.
.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 */
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).
.box {
position: static; /* default — no need to write this explicitly */
top: 50px; /* IGNORED — has no effect when position is static */
}
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.
.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;
}
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.
top: 10px; right: 10px
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).
/* 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.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.
/* 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, orright) for sticky to work — without an offset, the element behaves likerelative. - 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.
/* 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;
}
relative.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-indexonly works on positioned elements (relative,absolute,fixed,sticky). It has no effect onstaticelements.- Values can be negative — a negative
z-indexplaces 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.
(behind)
(middle)
(on top)
.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 */
}
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.
.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 */
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 |
| Property | Notes |
|---|---|
z-index | Controls stacking order. Only works on positioned elements (non-static). Higher = in front. Compared within a stacking context. |
inset | Shorthand 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.
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.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.<div class="card">
<span class="badge">New</span>
<div class="img-placeholder"></div>
<h3>Wireless Headphones</h3>
<p class="price">£89.99</p>
</div>
.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;
}
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.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..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 */
}
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.<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>
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;
}
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.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.<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>
.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;
}
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.