Positioning and Stacking
Some things on a page should not sit in the flow with everything else. A "New" badge belongs in the corner of a card, on top of it. A header can stay at the top of the window while the page scrolls under it. A message can float above everything. The position property takes an element out of its normal place, and a second idea, stacking, decides which element is drawn on top when two overlap.
Think of sticky notes on a noticeboard. A note is stuck to a spot regardless of the papers underneath, and the most recent note goes on top. The analogy holds until notes sit inside folders pinned to the board, and that exception turns out to be the one that matters most, at the end of this page.
relative: Moved from Where It Was
Every element starts as position: static, which means "in normal flow, where it would be anyway". The other values take four more properties, top, right, bottom and left, which say how far to move it and from where.
position: relative moves an element from where it would have been. top: 10px nudges it 10 pixels down, but the space it came from stays reserved, so nothing around it moves. On its own that is rarely useful. Its real job is to be the anchor for the next value.
absolute: Placed Inside an Anchor
position: absolute takes an element out of the flow completely. The elements around it close up as if it were not there, and it is placed using top, right, bottom and left, measured from the edges of its nearest ancestor that is itself positioned, meaning any position other than static. That is why the card below gets position: relative with no offsets: it moves nothing, and it becomes the anchor.
<article class="card"> <span class="badge">New</span> <h2>Weather Now</h2> <p>Shows today's forecast for any city.</p> </article>
.card {
position: relative;
padding: 1rem;
border: 1px solid black;
}
.badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0.25rem 0.5rem;
background: gold;
}
The browser shows a bordered card with its heading and text in their usual places, and a small gold label reading "New" sitting in the card's top-right corner, half a rem in from each edge. The heading does not move down to make room for the badge, because the badge is no longer in the flow. Delete the position: relative line from the card and the badge jumps to the top-right corner of the page, because there is no positioned ancestor left to anchor it.
fixed and sticky: Staying in View
position: fixed anchors an element to the browser window itself. It stays in the same spot on the screen however far the page scrolls, like a chat button in a corner. It is out of the flow, so the content it covers is hidden beneath it.
position: sticky is a hybrid. The element stays in normal flow, taking up its normal space, until scrolling would carry it past an edge you name, such as top: 0. From then on it sticks at that edge. It only sticks while its parent is still on screen; when the parent scrolls away, the sticky element goes with it.
.site-header {
position: sticky;
top: 0;
z-index: 1;
background: var(--color-bg);
}
These four lines join the site's existing header rule, or go in a second rule for the same class. When you scroll a long page, the header with your name and the nav stays pinned to the top of the window, and the page scrolls underneath it. The background line matters: without it the header is transparent, and text scrolling beneath shows through the nav links. The z-index line is explained in the next section; it keeps anything positioned further down the page sliding under the header rather than over it.
If a sticky element refuses to stick, check two things. It needs an edge, such as top, or it has nowhere to stick to. And no ancestor may have an overflow value such as hidden or auto, because that creates a new scrolling box the element sticks inside instead.
z-index and Stacking Contexts
When positioned elements overlap, the browser has to decide which is drawn on top. By default, the one that comes later in the HTML wins. The z-index property changes that on a positioned element: a higher number is drawn above a lower one.
That rule has an exception that causes most z-index frustration. Some elements create a stacking context, a separate layer whose children are stacked among themselves and then drawn together as one unit. A positioned element with a z-index creates one, and so do a few other properties, such as an opacity below 1. A child can never climb out of its parent's layer, whatever its own number.
<div class="panel"> <p class="popup">Popup</p> </div> <div class="banner">Banner</div>
.panel {
position: relative;
z-index: 1;
height: 6rem;
background: lightblue;
}
.banner {
position: relative;
z-index: 2;
height: 6rem;
background: gold;
}
.popup {
position: absolute;
top: 4rem;
left: 1rem;
z-index: 9999;
margin: 0;
padding: 1rem;
background: white;
}
A blue panel sits on the page with a gold banner directly below it, and a white popup inside the panel starts near the panel's bottom edge and hangs down into the banner. Its z-index is 9999, yet the bottom of the popup is hidden behind the gold banner. The popup lives inside the panel's layer, and the whole panel layer, at 1, is drawn below the banner, at 2. Remove the z-index: 1 line from the panel and it no longer makes a layer of its own. The popup's 9999 now competes directly with the banner's 2, and the whole popup appears on top.
So when a high z-index seems to do nothing, raising it further never helps. Look up the element's ancestors in the Elements panel for the one that created a layer, and fix the order there.
Use It Sparingly
Positioned elements ignore their neighbours. An absolute badge does not know the heading is there, and a fixed bar does not care what it covers. On a narrow phone, that is how text ends up hidden behind a badge or a banner covers half the screen. Flexbox and grid lay out the page; positioning is for the few exceptions that genuinely need to float.
- "
absoluteis measured from the page." It is measured from the nearest positioned ancestor. Only when there is none does it fall back to the page, which is why the card getsposition: relative. - "A bigger
z-indexalways wins." Only among elements in the same stacking context. Inside a lower layer, 9999 still loses to a 2 in a higher one. - "Positioning is how you lay out a page." Flexbox and grid lay out pages and adapt to every screen. Positioning is for exceptions like badges, sticky headers and overlays.
- Badges, sticky headers and overlays appear on almost every site, and each one is a few lines once you know which value to use.
- Knowing about stacking contexts ends the habit of typing ever larger
z-indexnumbers and hoping.
Knowledge Check
A badge has position: absolute; top: 0; right: 0. What is it placed against?
- The browser window, wherever the page has scrolled
- The nearest ancestor that has a position set
- The top-right corner of the whole page, always
- The spot where the badge would have been in the flow
What is the difference between sticky and fixed?
- Sticky is the newer name for fixed, with the same result
- Fixed keeps its place in the flow; sticky leaves it at once
- Sticky scrolls normally until it reaches an edge; fixed never moves
- Fixed only works on headers; sticky works on anything
A popup has z-index: 9999 but still appears under a banner with z-index: 2. What is the likely cause?
- Browsers ignore any z-index above one thousand
- The popup needs an even higher number, such as 99999 or more
- The banner comes later in the HTML, so it always wins
- An ancestor of the popup forms a lower stacking layer
A sticky header shows the page's text through it while scrolling. What is missing?
- A background colour on the header
- A top value for the sticky header
- Changing sticky to absolute instead
- An overflow value on a box around it
You got correct