Chapter 11 — Positioning and Z-Index
Section 1 — What positioning isPositioning is the CSS system for placing an element somewhere other than where normal flow would put it. In normal flow, elements stack and sit side by side according to the layout model of their parent — block stacking, flex arrangement, or grid placement — and each element's position is determined by the elements around it. Positioning breaks that relationship for a single element, letting you move it, fix it to the viewport, stick it to a scroll position, or layer it over other content, independently of the flow around it. It is the tool for the parts of a page that do not belong in the flow: badges on cards, dropdown menus, modal dialogs, fixed headers, tooltips, and close buttons — the elements that sit on top of, beside, or apart from the main content.
It is important to understand positioning's place among the layout tools. Flexbox and Grid arrange elements within a flow; positioning arranges elements outside it, or shifted within it. That makes positioning a complement to the layout systems, not a replacement for them: you would not build a whole page with absolute positioning, because absolutely positioned elements do not affect each other's positions, so a page of them would need every coordinate calculated by hand and would break at every screen size. But you would use positioning for the elements that need to escape the flow — a badge that should sit on a card's corner regardless of the card's content, a menu that should drop below a button regardless of the surrounding text — because flow-based layout cannot express "on top of" or "fixed to the viewport" at all. The skill is knowing which elements belong in the flow and which belong out of it, and positioning the second group deliberately while leaving the first group to Flexbox and Grid.
A useful mental image is to think of a page as layers. The bottom layer is the normal flow: the document, arranged by block, flex, and grid. On top of it sit positioned elements: the relative shifts that nudge things slightly, the absolute elements anchored to their containers, the fixed elements anchored to the viewport, the sticky elements that transition between flow and fixed. And above those, the stacking order — governed by z-index and stacking contexts — decides which layer paints on top when layers overlap. Positioning and z-index together are the layer system of CSS, and this chapter teaches that system as a whole, because the two cannot be understood separately: positioning creates the layers, and z-index orders them. Start with that image — a document, plus layers on top of it — and the details that follow will fall into place.
.badge {
position: absolute;
}
Positioning also matters for elements that are only temporarily visible, such as a menu that appears on hover or a panel that slides in from the side. Those elements need a position in the layer system even before they are visible, so that when they appear they land in the right place without pushing other content around.
Check: What kinds of page elements is positioning designed for, as opposed to Flexbox and Grid?
Section 2 — static: the default position`static` is the default value of `position`, and it means the element stays in normal flow, positioned by its parent's layout model, with no special behaviour. A static element cannot be moved with offsets — `top`, `right`, `bottom`, and `left` have no effect on it — and it does not participate in z-index stacking in the positioned sense, though it still paints in document order. Most elements on a page are static, and that is as it should be: the default is the flow, and positioning is the exception, applied only to elements that need to escape or shift within it. Understanding static matters because it is the baseline against which the other values are defined — every other value is a deviation from static, in a specific way, and knowing the baseline makes each deviation clear.
Static elements have two properties that matter for the rest of this chapter. First, a static element is not a positioned ancestor: an absolutely positioned descendant will look past it, through it, to the nearest ancestor that is positioned — that is, whose position is anything other than static — or to the initial containing block if no such ancestor exists. That rule, covered in detail in Sections 6 and 7, is the reason a wrapper must be given position: relative before an absolute child can be anchored to it: the wrapper must stop being static in order to become an anchor. Second, a static element does not create a stacking context on its own, so its z-index, if set, is ignored for stacking purposes — a detail that surprises developers who set z-index on a static element and wonder why it does not rise above its neighbours. Both properties follow from the same principle: static means "no positioning behaviour", and positioning behaviour is what creates anchors and stacking participation.
Static also interacts with the other layout models in a way worth noting: a static element inside a flex container is a flex item, and inside a grid container is a grid item, and its position is determined by those models, not by the position property. Positioning does not replace flex or grid arrangement for static elements; it only adds behaviour when the value is changed from static. A flex item with position: relative is still a flex item, arranged by the flex container, and then shifted from its flex position by its offsets — a combination covered in Section 3. A flex item with position: absolute leaves the flex arrangement entirely and is positioned by its offsets instead, as Section 5 explains. Static is thus the value that lets the layout models do their work undisturbed, and the other values are ways of modifying or escaping that work for specific elements.
/* Static is the default: no declaration needed. */
.element {
position: static;
}
Check: Do top, right, bottom and left offsets have any effect on a static element?
Section 3 — relative positioning`relative` positioning means the element stays in normal flow — it still occupies its original space, and other elements still flow around that original space — but it is visually shifted from its normal position by its offsets. Setting `top: 10px` on a relatively positioned element moves it 10 pixels down from where it would have been, leaving a gap where it was and overlapping whatever is below it, because the space it vacated is not reclaimed by its neighbours. That combination — flow position preserved, visual position shifted — is what makes relative positioning useful for small nudges: moving an icon a few pixels to align it with text, shifting a badge slightly, or adjusting an element's visual position without disturbing the layout around it. The shift is visual only; the layout behaves as if the element never moved.
The offsets for relative positioning work in the direction their names suggest, but with a detail that catches beginners: top: 10px moves the element down, not up, because it sets the distance from the element's top edge to where that edge would have been — pushing it down by 10 pixels. Similarly, left: 10px moves it right, bottom: 10px moves it up, and right: 10px moves it left. The offset names the edge that is pushed, not the direction of movement, and thinking of it as "push from this side" rather than "move toward this side" prevents the common error of writing top: -10px when top: 10px was meant, or vice versa. If both opposite offsets are set, such as top and bottom together, one wins according to the writing direction — in left-to-right pages, top wins over bottom and left wins over right — so setting both is usually a mistake, and a single offset per axis is the clean form.
Relative positioning's second, and arguably more important, use is as an anchor for absolutely positioned descendants. An element with position: relative and no offsets is visually identical to a static element — it does not move at all — but it is now a positioned ancestor, so an absolutely positioned child will anchor to it instead of to a more distant ancestor or the viewport. That is the standard pattern for badges, dropdowns, and overlays: the card or button gets position: relative with no offsets, purely to become an anchor, and the badge or menu inside it gets position: absolute with offsets that place it relative to the card or button. The relative declaration in that pattern is not about moving the card; it is about establishing a positioning context for its children, and recognising that dual role — shift and anchor — is essential for using relative positioning well. Many relative declarations in real code have no offsets at all, and they exist solely as anchors, which is a legitimate and common use.
.nudge {
position: relative;
top: 4px;
}
Check: When a relatively positioned element is shifted, what happens to the space it originally occupied?
Section 4 — Offsets: top, right, bottom and leftOffsets — `top`, `right`, `bottom`, and `left` — are the properties that place a positioned element, and they work differently for each position value, which is why they deserve a section of their own. For relative positioning, offsets shift the element from its normal position, as Section 3 explained. For absolute and fixed positioning, offsets place the element relative to its containing block, as Sections 5 and 8 explain: `top: 0` puts the element's top edge at the containing block's top edge, `left: 0` puts its left edge at the left edge, and so on. For sticky positioning, offsets set the threshold at which the element sticks, as Section 10 explains. The same four property names thus do three different jobs depending on the position value, and understanding which job applies is essential for using them correctly.
The values offsets accept include lengths, percentages, and the keyword auto. A length, such as 20px, is straightforward: 20 pixels from the relevant edge. A percentage is relative to the containing block's size on the corresponding axis — a percentage top or bottom is relative to the containing block's height, and a percentage left or right is relative to its width — which makes percentage offsets useful for centring and for proportional placement, but also a source of confusion when the containing block's size is not what you expected. auto, the default, means "no offset on this side", and it is what allows the other offset on the same axis to determine the position, or the element's natural position to show through. An absolutely positioned element with only top and left set, for example, has right and bottom as auto, so its width and height are determined by its content, not by stretching to the opposite edges.
Setting opposite offsets together has a special effect for absolute and fixed elements: it stretches them. An absolutely positioned element with both left: 0 and right: 0 will stretch horizontally to fill the containing block's width, minus any margins, because both edges are anchored and the element must span between them. Similarly, top: 0 and bottom: 0 stretches it vertically. That stretching is a useful technique for full-size overlays — an overlay with all four offsets at zero fills its containing block exactly — and for panels that should stretch between two edges, such as a sidebar that runs from the top of its container to the bottom. It also means that setting opposite offsets without intending to stretch can produce a surprisingly wide or tall element, and the fix is to set only one offset per axis, leaving the other as auto, so the element sizes to its content.
.overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
Check: What happens to an absolutely positioned element when both left and right are set?
Section 5 — absolute positioning`absolute` positioning removes the element from normal flow entirely: it no longer occupies space, other elements flow as if it were not there, and its position is determined by its offsets relative to its containing block. That removal from flow is what makes absolute positioning suitable for overlays, badges, dropdowns, and tooltips — elements that should sit on top of or beside the content without pushing it around. A badge absolutely positioned at a card's corner does not affect the card's text layout; a dropdown absolutely positioned below a button does not push the content below the button down; a modal absolutely positioned in its container does not disturb the page behind it. The element is out of the flow, and the flow proceeds without it, which is exactly the behaviour those components need.
The containing block for an absolute element is the nearest ancestor with a positioned value — relative, absolute, fixed, or sticky — or, if there is none, the initial containing block, which is essentially the viewport's document area. That rule is the key to absolute positioning, and it is covered in detail in Sections 6 and 7, because getting the containing block wrong is the most common absolute-positioning error: an element intended for a card's corner ends up at the page's corner, because the card was not positioned and the element anchored to a distant ancestor instead. The remedy is always the same: give the intended anchor position: relative, with no offsets if no shift is wanted, and the absolute child will anchor to it. That two-part pattern — relative anchor, absolute child — is the standard idiom of CSS positioning, and it appears in nearly every component that uses absolute positioning.
There are also implications of leaving the flow that go beyond layout. An absolute element does not contribute to its parent's height, so a container holding only absolute children will collapse to zero height unless it has its own height or other in-flow content — a classic surprise when a card's badge is absolute and the card's height is expected to include it. An absolute element's position does not change when surrounding content changes, so it can overlap content that grows into its space, which is sometimes intended for overlays and sometimes a bug for content that should push other content aside. And an absolute element's reading order remains its document order, regardless of its visual position, so a dropdown that appears below a button visually is still read where it sits in the HTML, which may be before or after the button depending on the markup. Those implications are manageable once known, but they are the reason absolute positioning should be reserved for elements that genuinely need to be out of flow, rather than used as a general layout tool.
.badge {
position: absolute;
top: 8px;
right: 8px;
}
Check: When an element becomes absolutely positioned, what happens to the space it occupied in the flow?
Section 6 — The containing blockThe containing block is the rectangle against which a positioned element's offsets are measured, and understanding it is essential for positioning, because every offset is relative to some containing block, and the wrong containing block produces the wrong position. For most elements, the containing block is their parent's content box — the area inside the parent's padding, where the parent's content lives. For absolutely positioned elements, the containing block is the nearest positioned ancestor's padding box — the area inside the ancestor's border, including its padding, which is a slightly larger rectangle than the content box, and that difference matters for precise placement at an ancestor's edges. For fixed elements, the containing block is usually the viewport, as Section 8 explains, with exceptions covered there. And for static and relative elements, offsets are relative to their own normal position, not to a containing block at all, which is why the containing block concept matters most for absolute and fixed positioning.
The padding-box detail deserves attention, because it affects where an absolute child sits when placed at an ancestor's corner. An absolute child with top: 0 and left: 0 sits at the top-left of the ancestor's padding box, which is inside the ancestor's border but outside its content — that is, it aligns with the inner edge of the border, not with the content's starting point after padding. If the ancestor has 20 pixels of padding, the child's top-left corner will be 20 pixels above and to the left of the content's top-left corner, overlapping the padding area. That is usually what you want for a badge at a card's corner, which should sit at the card's edge, not inset by the padding, but it can surprise you if you expected the child to align with the content. If you want the child aligned with the content, offset it by the padding amount, or place it inside an inner wrapper without padding. The containing block's definition — padding box, not content box — is the reason for the difference, and knowing it lets you predict the placement exactly.
The containing block can also be affected by transforms, filters, and other properties that create a containing block for fixed and absolute descendants, a topic that returns in Section 17's discussion of stacking contexts and in the fixed-position exceptions of Section 8. A transformed ancestor, for example, becomes the containing block for fixed descendants, which then position relative to the ancestor rather than the viewport — a behaviour that surprises developers who expect fixed to always mean viewport-relative. Those exceptions are edge cases for most layouts, but they are worth knowing as a category: certain CSS properties change what counts as a containing block, and when a positioned element lands in an unexpected place, checking whether an ancestor has a transform, filter, or perspective is a useful diagnostic step, alongside checking whether the intended anchor is positioned at all.
.card {
position: relative;
}
.card .badge {
position: absolute;
top: 0;
left: 0;
}
Check: For an absolutely positioned element, what is its containing block?
Section 7 — Absolute positioning and the nearest positioned ancestorAn absolutely positioned element anchors to its nearest positioned ancestor — the closest ancestor in the HTML tree whose position is relative, absolute, fixed, or sticky — and if no ancestor is positioned, it anchors to the initial containing block, which is the document's root area, effectively the page itself. That rule is simple to state and powerful in effect, because it means the anchor is determined by the HTML structure, not by proximity on screen: an absolute element inside a deeply nested structure will skip over static ancestors, however many there are, until it finds a positioned one, and that ancestor, wherever it is in the tree, becomes its frame of reference. The element's offsets are then measured from that ancestor's padding box, as Section 6 explained, and its position moves with that ancestor if the ancestor moves, scrolls, or resizes.
There is a subtlety about nesting and anchoring that is worth knowing: an absolute element's anchor is the nearest positioned ancestor, not the nearest ancestor of any kind, and not the element you might consider its "logical" parent. If a card contains a div, and the div contains a badge, and the card is relative but the div is static, the badge anchors to the card, skipping the div entirely. If the div is also relative, the badge anchors to the div, even though the card is also positioned and might seem like the more natural anchor. The rule is strictly nearest in the tree, and it does not consider visual proximity or logical grouping — only the ancestor chain and the position values along it. That strictness is actually helpful, because it makes anchoring predictable: walk up the tree from the absolute element, find the first positioned ancestor, and that is the anchor, every time, without exception.
A related subtlety is that an absolute element can itself be a positioned ancestor for its own absolute descendants, creating chains of anchoring: a modal anchored to the viewport contains a close button anchored to the modal, which contains a tooltip anchored to the button. Each level anchors to the nearest positioned ancestor above it, and the chain works because each level is positioned for its own sake and as an anchor for the next. That chaining is how complex layered interfaces are built — modals with buttons with tooltips, cards with badges with labels — and it follows the same nesting principle as flex and grid: each level manages its own children, and the anchor chain is just the positioning version of that principle. When a deeply nested absolute element lands wrong, walk the chain level by level, checking each ancestor's position value, and the broken link will reveal itself.
.menu-button {
position: relative;
}
.menu-button .dropdown {
position: absolute;
top: 100%;
left: 0;
}
Check: If no ancestor of an absolute element is positioned, what does it anchor to?
Section 8 — fixed positioning`fixed` positioning removes the element from flow and positions it relative to the viewport — the visible area of the browser window — so that it stays in the same place on screen even when the page scrolls beneath it. A fixed header stays at the top while the user scrolls down; a fixed action button stays at the bottom-right while the content moves; a fixed sidebar stays visible while the article scrolls. That scroll independence is fixed positioning's defining feature, and it is what distinguishes it from absolute positioning, which scrolls with its containing block, and from sticky positioning, which transitions between flow and fixed. If the requirement is "always visible, always in the same place on screen", fixed is the answer.
There are exceptions to the viewport rule that are worth knowing, because they produce the most confusing fixed-positioning bugs. If an ancestor has a transform, filter, perspective, or certain other properties, that ancestor becomes the containing block for fixed descendants, and the fixed element positions relative to the ancestor and scrolls with it, instead of staying fixed to the viewport. That behaviour is specified, not a browser bug, but it contradicts the intuition that fixed always means viewport-fixed, and it is a common cause of "my fixed header scrolls away" reports, where the header is inside a transformed wrapper. The fix is usually to move the fixed element outside the transformed ancestor, so that its containing block is again the viewport, or to remove the transform if it is not needed. When a fixed element misbehaves, checking its ancestors for transforms is a diagnostic step as important as checking its own offsets.
Fixed elements also raise design and accessibility considerations that go beyond their positioning. A fixed header or footer permanently occupies screen space, reducing the visible content area, which matters most on small screens where the viewport is already limited — a fixed header plus a fixed bottom bar can consume a large fraction of a phone's screen, leaving little room for content. Fixed elements can also obscure content beneath them, so pages with fixed headers need top padding on their content equal to the header's height, and pages with fixed footers need bottom padding, or content at the extremes will be hidden behind the fixed bars. And fixed elements that contain interactive controls must remain keyboard-accessible and must not trap focus inappropriately — a fixed modal, for example, needs focus management, while a fixed header's links should be reachable in a sensible tab order. Fixed positioning is powerful for persistent interface elements, but its persistence is also its cost, and that cost should be weighed for each use.
.site-header {
position: fixed;
top: 0;
left: 0;
right: 0;
}
Check: How does a fixed element behave when the page scrolls?
Section 9 — sticky positioning`sticky` positioning is a hybrid: the element stays in normal flow, occupying its space and flowing with its surroundings, until the page is scrolled to a threshold, at which point it sticks in place, behaving like a fixed element within its parent's bounds, and unsticks when its parent scrolls out of view. A section heading that sticks to the top of the viewport while its section scrolls, then is replaced by the next section's heading; a table header that sticks while the table's rows scroll; a sidebar that sticks while the article scrolls beside it — those are sticky elements, and they combine the best of flow and fixed: they do not permanently occupy screen space, they do not require content padding to avoid obscuring, and they return to the flow when their context passes. Sticky is the most recently introduced of the position values, and it has become a standard tool for scroll-aware interfaces.
The threshold is set by the offsets: top: 0 on a sticky element means "stick when the element's top would scroll above the viewport's top, and hold it at the viewport's top". A larger top value, such as top: 60px, holds it 60 pixels below the viewport's top, which is useful when a fixed header already occupies the top 60 pixels and the sticky element should sit below it rather than underneath it. The offset is thus a sticking distance, not a placement coordinate: it says how far from the viewport edge the element should stop, not where it should start. An element with no offset that would trigger sticking — for example, a sticky element with only left set in a vertically scrolling page — will never stick vertically, because there is no vertical threshold to cross, and it will behave like a relative element. At least one offset on the scrolling axis is required for sticking to occur.
The parent's bounds are the second crucial part of sticky behaviour. A sticky element can only stick within its parent's box: it sticks while its parent is in view, and when the parent's bottom edge reaches the sticky element's position, the element is pushed out with its parent, unsticking and scrolling away. That is why a sticky heading sticks only for its own section, not for the whole page — its parent is the section, and when the section ends, the heading leaves with it. It is also why a sticky element inside a parent that is only as tall as the element itself will never appear to stick: there is no room within the parent for the element to travel while stuck, so it never leaves its flow position. The parent must be taller than the sticky element, with scrollable room for the element to stick within, or sticking will not be visible. That parent-height requirement is the most common reason sticky "does not work", and checking the parent's height is the first diagnostic step.
.section-heading {
position: sticky;
top: 0;
}
Check: What limits how far a sticky element can stick?
Section 10 — Sticky thresholds and offsetsSticky thresholds are set by the offsets, and understanding them precisely is what makes sticky positioning predictable rather than mysterious. In a vertically scrolling page, `top` is the usual threshold: the element sticks when scrolling would take its top edge above the threshold distance from the viewport's top, and it remains stuck at that distance until its parent's bottom pushes it out. A `top` value of zero sticks the element flush to the viewport's top; a value of 80 pixels sticks it 80 pixels below the top, which is the right choice when a fixed header occupies the top 80 pixels and the sticky element should not slide underneath it. The threshold is thus a gap from the viewport edge, and choosing it is a matter of accounting for other fixed or sticky elements that already occupy that edge.
Multiple sticky elements can stack, and their thresholds determine how they interact. Two section headings with the same top: 0 will replace each other: the second heading, as it scrolls up, pushes the first out of the way, because the first cannot stick beyond its parent's bounds, and the second takes its place at the threshold. That replacement is the classic sticky-section-heading effect, and it works because each heading's parent is its own section, so each heading sticks only within its section and yields to the next. If the headings had different thresholds — one at 0, one at 40 pixels — they would stack, with the second sticking 40 pixels below the first, producing a layered stack of headings, which is sometimes the design intent for nested sections. The thresholds, combined with the parent bounds, fully determine the stacking behaviour, and sketching the sections and their thresholds on paper is a reliable way to predict it before writing code.
A final point about thresholds is that they are not animations: a sticky element does not glide to its stuck position, it snaps to it when the threshold is crossed, and it snaps back when the threshold is uncrossed. That snapping is usually imperceptible in normal scrolling, because the threshold crossing coincides with the element's natural scroll position, so the element appears to simply stop moving rather than to jump. But if the threshold is far from the element's natural position — for example, a top value much larger than the element's distance from the viewport top when it first appears — the element may appear to jump when it sticks, because it moves discontinuously from its flow position to its stuck position. Keeping thresholds close to the element's natural scroll path, or accounting for fixed headers that justify a larger threshold, avoids that jump, and it is a detail worth checking when a sticky element's behaviour looks jarring rather than smooth.
.sticky-nav {
position: sticky;
top: 64px;
}
Check: If a fixed header is 64 pixels tall, what top value should a sticky element below it use, and why?
Section 11 — Offsets only work on positioned elementsOffsets — top, right, bottom, and left — only affect elements whose position is not static. On a static element, they are ignored entirely, no matter what values they carry, because a static element's position is determined by the flow, and offsets are a positioning mechanism, not a flow mechanism. That rule is the source of a common beginner error: writing `top: 20px` on an element and wondering why it does not move, when the element's position is still the default static. The fix is to set a position value first — relative for a shift, absolute or fixed for placement, sticky for a threshold — and only then do the offsets take effect. Offsets without a position value are inert declarations, and recognising them as such saves a debugging session that would otherwise be spent adjusting numbers that cannot possibly work.
There is a related point about which offsets apply to which position values. For relative elements, all four offsets can be set, but opposite pairs compete, with one winning by writing direction, so a single offset per axis is the clean form. For absolute and fixed elements, all four can be set, and opposite pairs stretch the element, as Section 4 explained, so setting all four is a deliberate full-size pattern, not a mistake. For sticky elements, the offsets on the scrolling axis are thresholds, and offsets on the other axis are largely irrelevant to sticking, though they can affect the element's position within its parent in some cases. Knowing which offsets matter for each position value prevents the error of setting a full set of offsets on a sticky element and expecting them all to act as thresholds, or setting a single offset on an absolute element and expecting it to stretch.
A practical habit follows: whenever you write an offset, check that the same rule also sets a position value, and that the position value is the one whose offset behaviour you intend. That check takes a second and catches the most common positioning error at the point of writing, rather than at the point of debugging. It also makes stylesheets more readable: a rule that sets both position and offsets together states its complete positioning intent in one place, while a rule that sets offsets in one place and position in another — perhaps in a different stylesheet or a media query — forces the reader to assemble the intent from scattered declarations. Keeping position and its offsets together is a small discipline that pays off in readability and in fewer silent failures, because the two are a pair, and they should travel together.
/* Offsets need a position value to have any effect. */
.moved {
position: relative;
top: 10px;
}
Check: Why does top: 20px do nothing on its own?
Section 12 — z-index basics`z-index` controls the stacking order of positioned elements: which one paints on top when they overlap. An element with a higher z-index paints above one with a lower z-index, so a modal with z-index 100 appears above a header with z-index 10, and a dropdown with z-index 20 appears above content with z-index 1. The numbers themselves are arbitrary — they carry no units and no intrinsic meaning — and what matters is their relative order: higher means above, lower means below, and equal means document order decides, with later elements painting above earlier ones. Choosing a scale for z-index values is a project decision, covered in Section 32, but the basic mechanism is simply that comparison: higher wins, lower loses, equal falls back to order.
Z-index also interacts with document order in a way worth stating precisely. When two positioned elements have the same z-index, or when neither has a z-index set — in which case both are treated as auto, effectively zero for ordering purposes — the one later in the document paints above the earlier one. That fallback is why a dropdown that appears later in the HTML than the content it overlaps will naturally appear above that content even without a z-index, and why a modal at the end of the document will naturally cover earlier content. Explicit z-index values override that fallback, allowing an earlier element to paint above a later one if its value is higher, which is necessary when the document order and the desired stacking order disagree — for example, a header early in the document that must paint above content later in the document, such as a dropdown that extends below the header's bounds.
A common misunderstanding is that z-index is global — that a z-index of 100 anywhere on the page beats a z-index of 10 anywhere else. That is true only within the same stacking context, and stacking contexts, covered in Sections 16 to 18, partition the page into separate stacking worlds, within which z-index comparisons apply, but across which they do not. An element with z-index 100 inside a low stacking context can still paint below an element with z-index 1 in a higher context, because the contexts themselves are ordered, and the inner values never escape their context. That is the source of the most frustrating z-index bugs, where increasing a value seemingly without limit fails to bring an element forward, and the explanation is always a stacking context boundary. The basics of this section — higher wins, positioned only, document order as fallback — are true within a context, and the context rules of the later sections complete the picture.
.modal {
position: fixed;
z-index: 100;
}
The scale of numbers chosen for a project also affects how easy stacking is to debug. Small, spaced values, such as steps of ten, leave room to insert a new layer between two existing layers without renumbering everything. A scale that jumps straight to very large numbers, by contrast, leaves no room for nuance and makes every new layer feel like an escalation.
Check: Does z-index have any effect on a static element?
Section 13 — z-index only on positioned elementsThe rule that z-index requires positioning deserves its own section, because it is violated so often and its violation is so silent. Setting `z-index: 9999` on a static element does nothing at all — no error, no warning, no partial effect — and the element continues to paint in document order, below positioned elements that overlap it, regardless of the enormous number. That silence is what makes the bug frustrating: the developer increases the number, reloads, sees no change, increases it again, and concludes that z-index is broken, when in fact the element was never eligible for z-index ordering in the first place. The fix is a single declaration — a position value, usually relative for an element that should stay in flow but gain stacking, or absolute or fixed for an element that should leave flow as well — and once that declaration is present, the z-index takes effect immediately, often at a modest value that would have worked all along.
The reason for the rule is that z-index orders positioned layers, and a static element is not a layer: it is part of the flow, painted in document order as part of the normal painting sequence, without a separate stacking level to order. Positioned elements, by contrast, are painted in a separate phase of the painting order, after the flow content, and within that phase they are ordered by z-index and document order. That painting model is why positioned elements naturally appear above static content even without a z-index — they are painted later — and why a static element cannot be lifted above a positioned one by z-index alone: the two are in different painting phases, and the positioned phase comes after the flow phase. To move a static element into the positioned phase, you must position it, which is a small price for the control z-index provides.
There are exceptions to the positioned-only rule, introduced by properties that create stacking contexts or make an element participate in stacking without a position value. An element with opacity less than 1, a transform, a filter, or certain other properties becomes a stacking-context participant even if static, and its z-index may then have an effect in some browsers and contexts — but relying on those exceptions is fragile, because their behaviour varies and they create stacking contexts as a side effect, with consequences covered in Section 17. The reliable rule, and the one to teach and to follow, is that z-index goes with positioning: if you want to order an element, position it first, explicitly, and do not depend on a side effect of another property to make it eligible. That discipline keeps stacking predictable and avoids the class of bugs where an element's stacking changes unexpectedly when an unrelated property — an opacity fade, a transform animation — is added or removed.
/* z-index needs a position value to work. */
.dropdown {
position: absolute;
z-index: 20;
}
Check: What single declaration must be added before z-index can lift a static element?
Section 14 — Stacking order and document orderWhen positioned elements overlap and their z-index values do not decide the order — because they are equal, or because neither has a z-index set — the document order decides: later elements paint above earlier ones. That fallback is the default stacking behaviour of the web, and it reflects a simple principle: content added later is assumed to be more recent, more specific, or more important, and it should therefore appear on top. A dropdown defined after the content it overlaps will cover that content; a modal defined at the end of the document will cover everything before it; a tooltip defined after its trigger will appear above neighbouring content. That natural behaviour is why many simple overlays work without any z-index at all, as long as their HTML comes after the content they should cover.
Negative z-index values add a further layer to the ordering. An element with a negative z-index paints below the normal flow content of its stacking context, but above the context's background — a position that is useful for decorative elements that should sit behind content but above the page background, such as a large background shape or a watermark. Section 15 covers negative values in detail, but the point here is that the ordering scale extends below zero, and the document-order fallback applies within each z-index level, including negative ones: two elements both at z-index -1 paint in document order relative to each other, with the later one above. The full ordering, within a stacking context, is therefore: backgrounds, then negative z-index elements in document order, then flow content, then positioned elements with z-index auto in document order, then positioned elements with positive z-index in ascending order and document order within each value. That sequence is the painting order, and it is worth learning as a whole, because it explains every stacking result within a context.
Understanding the painting order also clarifies why positioned elements without a z-index appear above static content: they are in a later painting phase than flow content, regardless of document order, so a positioned element early in the document can still cover static content later in the document, because the phases, not just the order within a phase, determine the result. That phase distinction is the reason the positioned-only rule for z-index matters: static and positioned elements are not in the same ordering competition, and z-index only orders within the positioned phase. The painting order is thus a layered system — backgrounds, negative layers, flow, positioned layers — and z-index is the ordering within the positioned layers, not a global ordering across all layers. Keeping that layered image in mind prevents the error of expecting a z-index to lift a flow element above a positioned one, or vice versa, without first moving the element into the right layer.
/* Later in the document paints above, when z-index values tie. */
Check: When two positioned elements have the same z-index, which one paints on top?
Section 15 — Negative z-indexA negative z-index places an element below the normal flow content of its stacking context, but above that context's background. That is a specific, useful position: behind the text and boxes, but in front of the background colour or image, so that a decorative shape, a watermark, or a background pattern can sit behind content without disappearing behind the page background entirely. A large, soft circle behind a heading, a diagonal stripe behind a card, or a faint pattern behind a section — those are negative z-index elements, positioned absolutely within their container, given a negative value, and left to paint behind the content but above the container's background. Without a negative z-index, such a decoration would need to be a background image or a separate layer; with it, it can be an ordinary element, styled and positioned like any other, simply ordered behind.
The requirements for negative z-index are the same as for positive: the element must be positioned, and it must be within a stacking context whose background it should sit above. If the element's parent creates a stacking context with its own background, the negative element will paint above that background but below the parent's content, which is usually the desired result. If the parent does not create a stacking context, the negative element may paint below the parent's background as well, disappearing entirely behind an ancestor's background — a classic negative z-index bug, where the decoration vanishes because it went behind a background it was meant to sit above. The fix is usually to ensure the parent creates a stacking context, or to place the decoration in a context where its background relationship is correct, rather than increasing the negative value, which only moves it further behind. Negative values go further down, not up, and a disappearing negative element is almost always a context problem, not a value problem.
Negative z-index also interacts with interactivity in a way worth knowing. An element behind the flow content is also behind it for hit-testing: clicks and taps will hit the content above it, not the negative element beneath, which is usually correct for a decoration that should not be interactive. But if the negative element contains a link or a button that should be clickable, placing it behind content will make that control unreachable, because the content above intercepts the clicks. Negative z-index is therefore for non-interactive decoration, not for controls, and any interactive element placed behind content should be reconsidered — either brought forward with a positive or auto z-index, or redesigned so that its interactivity does not depend on being behind something else. The visual layer and the interaction layer should agree, and a control hidden behind content for visual reasons but expected to be clickable is a contradiction that users will experience as a broken button.
.decoration {
position: absolute;
z-index: -1;
}
Check: Where does an element with a negative z-index paint relative to its context's content and background?
Section 16 — Stacking contextsA stacking context is a self-contained stacking world: a group of elements whose z-index values are compared only with each other, and whose whole group is then ordered as a single unit against other groups. The page's root creates the root stacking context, and certain elements create nested contexts inside it — a positioned element with a z-index other than auto, an element with opacity less than 1, a transformed element, and several other cases covered next. Within a context, the painting order from Section 14 applies: backgrounds, negative layers, flow, positioned layers in z-index order. Across contexts, only the contexts' own order matters: a context with a higher order paints entirely above a context with a lower order, regardless of the z-index values inside them. That encapsulation is the single most important concept in z-index, because it explains why a huge z-index can fail to bring an element forward — the element is trapped inside a low context, and its value never escapes to compete globally.
An analogy helps: think of stacking contexts as sealed boxes, each containing a stack of papers, and the boxes themselves stacked on a table. Rearranging the papers inside one box changes their order within that box, but it cannot move a paper from a lower box above a paper in a higher box, because the boxes are sealed — the papers never leave their box, and only the boxes are ordered on the table. To move a paper above a paper in another box, you must move its whole box above the other box, by changing the box's own order, not the paper's order within its box. In CSS terms, to bring an element forward across a context boundary, you must raise its context's z-index — the positioned ancestor that created the context — not the element's own z-index, which only orders it within its context. That analogy, kept in mind, resolves the majority of "z-index not working" problems, because those problems are almost always attempts to reorder papers across sealed boxes.
The practical approach to stacking contexts is to design them deliberately rather than accumulating them accidentally. Keep the number of contexts small and meaningful: the page's root, a modal layer, a header layer, a dropdown layer — each a positioned element with a deliberate z-index, creating a context for its contents. Avoid creating contexts unintentionally on elements that contain overlapping children, by being aware of the context-creating properties and their effects. And when a stacking problem arises, identify the contexts involved before adjusting values: which context contains the element, which context contains the element it should rise above, and what orders those contexts relative to each other? Answering those three questions locates the fix — usually at the context level, not the element level — and prevents the futile escalation of z-index values that is the hallmark of a context problem being treated as a value problem.
/* A positioned element with a z-index creates a stacking context. */
.modal-layer {
position: fixed;
z-index: 100;
}
Check: Why can an element with z-index 1000 still paint below an element with z-index 1?
Section 17 — What creates a stacking contextStacking contexts are created by a list of properties and values that is longer than most developers expect, and knowing the list — or at least its most common members — is essential for diagnosing stacking problems. The classic creator is a positioned element with a z-index other than auto: any positioned element given a numeric z-index, including zero, creates a context for its descendants. The root element always creates a context. Beyond those, the common creators include: opacity less than 1; transform other than none; filter other than none; perspective other than none; clip-path other than none; mask or masking properties; mix-blend-mode other than normal; isolation set to isolate; will-change set to certain values; and contain set to certain values. Each of these creates a context because its rendering requires the element and its descendants to be composited as a unit, and that compositing unit is, in effect, a stacking context.
The practical impact is that ordinary styling can create contexts unintentionally. A card with a hover effect that uses transform to lift it, and opacity to fade it, creates a context around its children, which may include a dropdown or badge that should stack above neighbouring cards — but inside the card's context, the dropdown's z-index only competes within the card, and the card's own stacking determines whether the dropdown appears above the next card. If the cards are in document order and the hovered card is earlier, its dropdown may paint below the next card, even with a high z-index, because the next card's context paints above the hovered card's context. The fix is not to raise the dropdown's z-index but to raise the hovered card's stacking — for example, by giving the hovered card a z-index, or by restructuring so the dropdown is not inside a context-creating card. That example, which occurs frequently in real interfaces, shows why context awareness matters more than z-index values.
The guideline is not to avoid these properties — they are essential tools — but to be aware of their context-creating side effect and to design stacking with it in mind. When you add a transform, filter, or opacity to an element that contains positioned children, ask whether those children need to stack outside the element, and if so, consider whether the property is necessary, whether the children can be moved outside the element, or whether the element's own stacking can be raised to compensate. And when debugging a stacking problem, check the ancestors for context-creating properties, not just for z-index values, because the context creator may be an opacity or transform that has nothing to do with stacking in the developer's mind, but everything to do with it in the browser's rendering. That check — ancestors' properties, not just the element's own — is the diagnostic habit that separates stacking experts from developers who escalate z-index values in frustration.
/* Opacity below 1 creates a stacking context, even without z-index. */
.faded {
opacity: 0.9;
}
Check: Name three properties, other than z-index, that can create a stacking context.
Section 18 — z-index within a context versus across contextsZ-index comparisons apply only within a stacking context, and that single rule governs all cross-context behaviour. Inside one context, higher beats lower, ties fall back to document order, and negative values sit below flow content, as Sections 12 to 15 described. Across contexts, the contexts themselves are ordered by their own z-index values and document order, and the inner values are irrelevant to that ordering: a context's entire contents, including an element at z-index 1000, paint as a unit at the context's level, so if the context is below another context, all of its contents are below all of the other context's contents, no matter how high their inner values are. That is why the sealed-box analogy from Section 16 is accurate: inner order never crosses the box's boundary, and only the boxes are ordered against each other.
A concrete example makes the rule tangible. Suppose a page has a header with z-index 10, creating a context, and a main area with no z-index, containing a modal with z-index 1000. The modal's 1000 only orders it within the main area's context, which is the root context, so it competes with the header's context at the root level — and the header's context, at z-index 10, is a unit that paints above the root's flow content, but the modal, at 1000 within the root, paints above the header's context, because the root's positioned layers are ordered by z-index and 1000 beats 10. That example works as expected. Now suppose instead that the modal is inside a card with z-index 1, creating a context, and the header is at z-index 10. The modal's 1000 orders it within the card's context, but the card's context, at z-index 1, paints below the header's context at 10, so the modal — despite its 1000 — paints below the header. The fix is to raise the card's context to above 10, or to move the modal outside the card, not to raise the modal's value further, because no inner value can overcome the context ordering.
When debugging a cross-context problem, the routine is to identify the contexts, not to escalate values. Find the element's nearest stacking-context ancestor, find the other element's nearest stacking-context ancestor, and compare those ancestors' order: their z-index values, their document order, and whether they are in the same parent context. If the ancestors' order is wrong, fix it — raise the ancestor's z-index, reorder the ancestors, or move the element to a different context. If the ancestors' order is correct but the element still appears wrong, then the problem is within a context, and the element's own z-index or document order is the fix. That two-level diagnosis — context first, then value — is the systematic approach to stacking, and it replaces the trial-and-error escalation that wastes time and produces stylesheets full of enormous, meaningless z-index numbers.
/* The card's context (z-index 1) traps its modal below the header (z-index 10). */
.card {
position: relative;
z-index: 1;
}
Check: To bring an element above another element in a different stacking context, what must you change?
Section 19 — Building a dropdown menuA dropdown menu is the classic absolute-positioning component: a button that, when activated, reveals a menu positioned below it, overlapping the content beneath, without pushing that content down. The structure is a wrapper with `position: relative`, containing the button and the menu, with the menu given `position: absolute`, `top: 100%`, and `left: 0`, so that its top edge aligns with the wrapper's bottom edge and its left edge aligns with the wrapper's left edge. The `top: 100%` is the key: 100 percent of the wrapper's height, placing the menu exactly below the wrapper, regardless of the button's height, so the menu follows the button if the button's size changes. The menu is hidden by default — with display, visibility, or a class — and shown when the button is activated, by JavaScript toggling a class, or by CSS for a hover-only menu, though hover-only menus are not accessible to keyboard or touch users and should not be the sole mechanism.
The dropdown's stacking must be considered: the menu should paint above the content below it, which it will do naturally if it is positioned and later in the document than that content, or if it has a z-index that lifts it within its context. If the dropdown is inside a header with a stacking context, the menu's z-index only orders it within the header's context, and the header's context must itself be above the main content's context for the menu to appear above the main content — a context consideration from Section 18 that applies to every dropdown inside a header or card. Testing a dropdown therefore includes scrolling the page and checking that the menu appears above all content it overlaps, not just the content immediately below the button, because a menu that is clipped or covered by a neighbouring section is a broken dropdown, even if it appears correctly in isolation.
Accessibility for dropdowns is as important as their positioning, and it is where many dropdown implementations fail. The button should indicate that it controls a menu, through an aria-expanded attribute that toggles with the menu's state, and an aria-haspopup or equivalent that identifies the menu. The menu's items should be focusable and keyboard-operable: arrow keys to move between items, Escape to close and return focus to the button, and Tab to move out of the menu in a sensible order. The menu should close when the user clicks outside it, presses Escape, or activates an item, and focus should return to the button when it closes, so keyboard users are not left stranded. Those behaviours require JavaScript, and they are part of building a dropdown, not optional extras — a dropdown that only works with a mouse is not finished, because a significant portion of users navigate by keyboard, touch, or assistive technology, and each of those input methods needs a working path through the menu.
.dropdown-wrapper {
position: relative;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 20;
}
Check: Why does a dropdown menu use top: 100% rather than a fixed pixel value?
Section 20 — Building a modal overlayA modal overlay is a dialog that covers the page, dimming the content behind it and focusing the user's attention on a task — confirming an action, filling in a form, or viewing details — until it is dismissed. The structure is typically two elements: a backdrop that covers the viewport, dimming it with a semi-transparent background, and a dialog box centred within the backdrop, containing the modal's content. The backdrop is usually `position: fixed` with all four offsets at zero, so it fills the viewport and stays in place while the page scrolls, and with a z-index high enough to place it above all page content, in a top-level layer outside any component context, as Section 18 recommends. The dialog is centred within the backdrop, either as a flex or grid child of the backdrop, or as an absolutely positioned element centred with offsets and margins, and it has its own background, padding, and shadow to distinguish it from the dimmed page behind.
The modal's stacking is critical: it must be above everything, including fixed headers, dropdowns, and other overlays, which means its layer's z-index must exceed theirs, and its layer must not be trapped inside a lower stacking context. Placing the modal's elements at the end of the document, in a dedicated container, is the standard way to ensure that, because the root context's ordering then applies directly, and the modal's high z-index competes globally. If the modal is instead nested inside a component — a card, a section, a header — its stacking is trapped in that component's context, and it may paint below other page elements despite a high value, producing the classic "modal behind the header" bug. That bug is a context bug, not a value bug, and its fix is architectural — move the modal to the top layer — not numerical.
There are also interaction details that make a modal feel correct. Clicking the backdrop — outside the dialog — should usually close the modal, though some modals, such as those confirming a destructive action, deliberately require an explicit button press and do not close on backdrop click, and that choice should be deliberate and communicated. The page behind the modal should not scroll while the modal is open, which is usually achieved by preventing body scrolling, so that the modal remains the focus of attention and the background does not shift beneath it. And the modal's size should adapt to its content and to the viewport, with a maximum width and height and internal scrolling for long content, so that it remains usable on small screens where a large dialog would otherwise overflow. Those details, combined with the positioning and stacking foundations, turn a fixed overlay into a modal that users can actually use, rather than merely see.
.modal-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
}
Check: Why should a modal's elements be placed at the end of the document rather than nested inside a component?
Section 21 — Building a tooltipA tooltip is a small label that appears when a user hovers over or focuses on an element, providing extra information — the full name behind an abbreviation, the purpose of an icon button, or a hint about a control — without cluttering the interface permanently. The structure is a wrapper with `position: relative`, containing the trigger element and the tooltip itself, with the tooltip given `position: absolute` and offsets that place it above, below, or beside the trigger, most commonly above, centred on the trigger's horizontal centre. The tooltip is hidden by default and shown on hover and focus, so that both mouse and keyboard users can reveal it, and it is positioned to avoid covering the trigger itself, so that the user can still see what the tooltip describes. That combination — relative wrapper, absolute tooltip, show on hover and focus — is the standard tooltip pattern, and it appears in countless interfaces in exactly that form.
Positioning a tooltip precisely requires care with its offsets and its alignment. A tooltip above its trigger might use bottom: 100% to sit above the wrapper, plus left: 50% and a transform to centre it horizontally — transform: translateX(-50%) shifts it left by half its own width, centring it on the trigger's centre, regardless of the tooltip's width, which varies with its text. That transform-centring technique is elegant, but it creates a stacking context on the tooltip, as Section 17 noted, which is usually harmless for a tooltip but worth knowing if the tooltip contains overlapping children. Alternatively, the tooltip can be positioned with fixed offsets and a known width, avoiding the transform, at the cost of flexibility when the text length changes. Either approach works, and the choice depends on whether the tooltip's width is fixed by design or variable by content.
Tooltips raise accessibility questions that are more subtle than they first appear. A tooltip that appears only on hover is inaccessible to keyboard users and to touch users, who cannot hover, so the tooltip must also appear on focus, when the trigger receives keyboard focus, and it should remain visible while the trigger is focused, so keyboard users can read it. A tooltip that contains essential information — information the user needs to complete a task — should not be tooltip-only: essential information belongs in visible text, or in a form that assistive technology can access reliably, because tooltips are often not announced by screen readers, or are announced inconsistently, depending on how they are implemented. The recommended pattern is to use tooltips for supplementary, non-essential hints, and to provide essential information in the main content, or through an accessible name or description that screen readers reliably convey. A tooltip that duplicates an accessible name, such as the label of an icon button, is a good use; a tooltip that is the only place a form field's requirement is stated is not.
.tooltip-trigger {
position: relative;
}
.tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
}
Check: Why must a tooltip appear on focus as well as on hover?
Section 22 — Building a fixed headerA fixed header is a header that stays at the top of the viewport while the page scrolls beneath it, keeping navigation, branding, or actions always reachable. The implementation is straightforward: the header gets `position: fixed`, `top: 0`, `left: 0`, and `right: 0`, so it spans the viewport's width and sits at its top, with a z-index that places it above the scrolling content, and a background that obscures the content scrolling beneath it — without a background, the content would show through the header, producing a confusing overlap. The page's content then needs top padding equal to the header's height, so that the first content is not hidden behind the fixed header when the page loads at the top, and so that anchored links and headings do not scroll underneath it. That padding is part of the fixed-header pattern, not an optional extra, and forgetting it is the most common fixed-header bug, producing a page whose top content is permanently hidden.
Fixed headers also interact with other fixed and sticky elements, and their z-index values must be coordinated. A fixed header at z-index 50 will paint below a modal at z-index 100, which is correct — the modal should cover the header — but above page content at the default level, which is also correct. A dropdown inside a fixed header, however, must paint above the page content below the header, which requires the header's context to be above the content's context, or the dropdown to be placed outside the header's context, as Section 18 explains. Testing a fixed header therefore includes opening its dropdowns and checking that they appear above the page content, not clipped by the header's bounds or covered by the next section, because a header whose dropdowns are hidden is a header that has failed at its primary job of providing navigation.
On small screens, a fixed header's cost becomes acute: it permanently occupies vertical space that is already scarce, and a header that is comfortable on a desktop monitor can consume a fifth or more of a phone's screen, leaving little room for content and making the page feel cramped. The remedies are to reduce the header's height on small screens, to hide less essential header elements at narrow widths, or to switch the header to sticky or static on small screens, so that it scrolls away and frees the space. Those responsive decisions should be made deliberately, with the header tested at phone widths, because a fixed header that works on desktop but dominates a phone screen is a header that has not been designed responsively. Fixed positioning is a powerful tool for persistence, but persistence should be weighed against screen space, and on small screens, the balance often tips toward letting the header scroll.
.site-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 50;
}
Check: Why does page content need top padding when a header is fixed?
Section 23 — Sticky section headings and table headersSticky section headings are the showcase use of sticky positioning: each section's heading sticks to the top of the viewport while its section scrolls, then yields to the next section's heading, so that the user always knows which section they are reading, even deep inside a long page. The implementation is a heading with `position: sticky` and a top threshold — usually zero, or the height of a fixed header above it — inside a section that is taller than the heading, so that there is room for the heading to stick within its parent's bounds. As the user scrolls, the heading scrolls with its section until it reaches the threshold, then sticks, remaining visible while the section's content scrolls beneath it, and when the section ends, the heading is pushed out by the next section's heading, which takes its place. That handover, governed by parent bounds and thresholds as Sections 9 and 10 explained, produces the smooth, self-updating section indicator that users recognise from documentation sites and long articles.
Sticky table headers apply the same mechanism to data tables: the header row of a long table sticks to the top while the table's rows scroll, so that column labels remain visible and the data remains interpretable, no matter how far down the table the user has scrolled. The implementation is a header cell or row with position: sticky and a top threshold, inside a table that scrolls — either because the page scrolls or because the table is inside a scrolling container — with a background on the sticky cells, so that the rows scrolling beneath do not show through them. The background is essential: without it, a sticky header is transparent, and the scrolling rows will be visible through it, producing a confusing double image. With a solid background, the header reads as a fixed bar, and the table remains usable at any scroll position. That background detail is easy to forget and immediately visible when forgotten, so it should be part of the sticky table header pattern from the start.
There are details that make sticky headings work well in practice. The sticky element needs a z-index if it should paint above the content scrolling beneath it — without one, later content may paint above it, depending on document order and positioning, and the heading may appear to be covered by the text scrolling under it. The heading's background, like the table header's, should be solid, so that content beneath does not show through. And the heading's threshold should account for any fixed header above it, so that the sticky heading stops below the fixed header rather than sliding underneath it, as Section 10 described. Those three details — z-index, background, threshold — are the complete checklist for a sticky heading that looks and behaves correctly, and they apply to section headings, table headers, and any other sticky element that should remain legible while content scrolls beneath it.
.table-header {
position: sticky;
top: 0;
background: #fff;
z-index: 10;
}
Check: Why does a sticky table header need a solid background?
Section 24 — Badges on cardsA badge on a card — a "New" label, a discount percentage, a notification count, or a status indicator — is the textbook absolute-positioning component: a small element that should sit at a precise spot on the card, usually a corner or an edge, regardless of the card's content, without affecting the card's layout. The pattern is a card with `position: relative`, containing the badge with `position: absolute` and offsets that place it, such as `top: 8px` and `right: 8px` for the top-right corner, or negative offsets like `top: -8px` for a badge that deliberately overlaps the card's edge, sitting half outside it for a sticker-like effect. The badge does not participate in the card's flex or grid layout, so it never pushes the card's text or image around, and the card's size is determined by its in-flow content alone, which is exactly the behaviour a badge needs.
The choice of offsets determines the badge's visual relationship to the card. Positive offsets inset the badge from the edge, placing it inside the card with a small margin, which suits badges that should feel contained, such as a category label on an image. Zero offsets place it flush with the edge, which suits badges that should align with the card's border. Negative offsets push it outside the edge, which suits badges that should feel like stickers or ribbons, breaking out of the card's rectangle for emphasis. Each choice is a design decision, and the offsets are the mechanism for expressing it, with the card's padding box as the frame of reference, as Section 6 explained — a badge at top: 0 sits at the padding box's top edge, inside the border, not at the content's top edge after padding, which is usually the intended corner. Testing a badge at different card sizes confirms that it stays in its corner regardless of content, which is the property that makes absolute positioning the right tool for it.
There is also the question of what happens when a card has multiple badges, or when a badge's text is longer than expected. Multiple badges can be stacked in a corner with a small gap, or placed at different corners, but they should not overlap each other or the card's essential content, such as its title or its primary button, because an overlapping badge can obscure text or intercept clicks meant for the content beneath. A badge with long text should wrap or be constrained with a max-width, rather than stretching across the card and covering its image, and the badge's font size should be chosen so that its text remains legible at the badge's small size. Those are design details, but they follow from the positioning choice: because the badge is out of flow, it will not push content aside when it grows, so its growth must be anticipated and constrained, or it will cover what it was meant to annotate.
.card {
position: relative;
}
.card .badge {
position: absolute;
top: 8px;
right: 8px;
}
Check: Why is absolute positioning the right tool for a badge on a card?
Section 25 — Close buttons on cards and dialogsA close button — the small "×" in the corner of a card, a dialog, a notification, or a banner — is another classic absolute-positioning component, and it follows the same pattern as a badge: a container with `position: relative`, and a button with `position: absolute` and corner offsets, placed where users expect to find it, usually the top-right corner in left-to-right interfaces. The button's placement is independent of the container's content, so it stays in its corner regardless of how much text the card or dialog contains, and it does not push the content around, which is essential for a control that should be consistently locatable — users learn where close buttons live, and a button that moves with content length breaks that learned expectation. Consistency of placement is the close button's primary requirement, and absolute positioning delivers it.
The close button's accessibility is more demanding than a badge's, because it is interactive. It must be a real button element, not a styled div or span, so that it is keyboard-focusable and activatable by Enter and Space, and announced as a button by assistive technology. It needs an accessible name, because "×" alone is not a meaningful label: an aria-label such as "Close" or "Dismiss" gives screen reader users the button's purpose, while sighted users see the symbol. It needs a visible focus style, so keyboard users can see when it is focused, and a hit area large enough to tap comfortably on touch screens — at least the size of a fingertip, even if the symbol itself is small, achieved with padding around the symbol. And its action must be clear: closing a dialog should return focus to the element that opened it, closing a notification should remove the notification without losing the user's place, and closing a card should not leave a confusing gap or a broken layout. Those behaviours are part of building a close button, not extras, and they apply whether the button is absolutely positioned or not — positioning only determines where it sits, not how it behaves.
Finally, close buttons illustrate a broader principle about absolute controls: their position is fixed, but their context is not. A close button on a dialog must close that dialog, not another one; a close button on a notification must dismiss that notification, not all of them; and that association is established by the HTML structure and the JavaScript behaviour, not by the CSS positioning. Positioning places the button visually, but the button's meaning comes from its markup and its handler, and a button that looks like it belongs to a dialog but actually closes a different element is a broken control, however well placed it is. That separation — visual placement by CSS, behaviour by markup and script — is a healthy way to think about all positioned controls: CSS puts them where they belong, and the rest of the implementation makes them do what they should.
.dialog {
position: relative;
}
.dialog .close-button {
position: absolute;
top: 8px;
right: 8px;
}
Check: What must a close button have, besides its position, to be usable by keyboard and screen reader users?
Section 26 — Centring an absolutely positioned elementCentring an absolutely positioned element can be done in several ways, each suited to a different situation, and knowing all of them lets you choose the one that fits the element's size and context. The transform method is the most general: set `top: 50%` and `left: 50%`, which places the element's top-left corner at the containing block's centre, then apply `transform: translate(-50%, -50%)`, which shifts it back by half its own width and height, centring it exactly, regardless of its size. That method works for elements of unknown or variable size — a modal whose content determines its dimensions, a tooltip whose text length varies — because the transform percentages are relative to the element's own size, not the container's, so the centring adapts automatically. Its cost is that the transform creates a stacking context on the element, which is usually harmless for a centred overlay but worth knowing if the element contains overlapping children.
The opposite-offsets plus auto-margins method is the alternative for elements of known size, or for elements that should stretch: set all four offsets to zero and margin: auto, and the browser centres the element by distributing the spare space equally as margins, provided the element has a definite width and height — without definite dimensions, the element stretches to fill the containing block instead of centring, which is the full-size overlay pattern from Section 4, not a centred box. That method does not create a stacking context, which can be an advantage, and it reads clearly as "centred by margins", but it requires known dimensions, which makes it less flexible than the transform method for content-sized elements. For a dialog with a fixed width and height, it is clean and reliable; for a dialog whose height varies with content, the transform method is better, because it does not require a height to be stated.
The modern flex and grid methods centre an absolute element indirectly, by centring it within a container that is itself a flex or grid container: a fixed backdrop with display: flex and centred justification and alignment will centre its dialog child, even if the dialog is not itself positioned, because the backdrop's layout centres it. That method is now the most common for modals, because the backdrop is already a fixed overlay, and making it a flex container centres its dialog with two declarations and no transforms or margins. It also handles the overflow case more gracefully: a dialog taller than the viewport in a flex backdrop can be scrolled or aligned to the top with a single alignment change, while a transform-centred dialog may overflow at the top where it cannot be scrolled to. The choice among the three methods is therefore not just about centring but about overflow, size variability, and stacking, and the flex backdrop method wins on most of those counts for modals, while the transform method wins for standalone absolute elements that must centre within an arbitrary containing block.
.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
Check: Why does the transform centring method work even when the element's size is unknown?
Section 27 — The inset shorthand
The inset property is shorthand for all four offsets — top, right, bottom, and left — in that order, so inset: 0 sets all four to zero, inset: 10px 20px sets top and bottom to 10 pixels and right and left to 20, and inset: 10px 20px 30px 40px sets each individually, following the same one-to-four-value pattern as margin and padding. It is a convenience, not a new capability: every inset declaration can be expanded into four offset declarations with identical effect, and the shorthand exists to make full-size overlays, stretched panels, and uniform insets shorter to write and easier to read. inset: 0 for a full-size overlay is the canonical example — four offsets in two words — and it appears constantly in modern stylesheets for backdrops, overlays, and stretched elements, replacing the four-line offset block that older code required.
The shorthand also has logical variants, inset-block and inset-inline, which set the offsets on the block and inline axes respectively, following the writing direction rather than physical directions. In a left-to-right, top-to-bottom page, inset-block sets top and bottom, and inset-inline sets left and right, but in a right-to-left page, inset-inline's start and end swap, so that the same declaration produces a mirrored layout without changes. Those logical properties are part of the broader move in CSS toward direction-agnostic layout, alongside logical margins and padding, and they are worth knowing for interfaces that must work in multiple writing directions, even if physical inset is sufficient for most single-language projects. The physical shorthand is the common form; the logical forms are the internationalised form; and both follow the same value pattern.
There are also individual logical offset properties — inset-block-start, inset-block-end, inset-inline-start, inset-inline-end — which set a single logical edge, and which are useful when only one edge's offset matters and its physical direction depends on the writing mode. A dropdown that should sit below its trigger in any writing direction might use inset-block-start: 100% rather than top: 100%, so that it sits below in both left-to-right and right-to-left layouts without a separate rule. Those properties are advanced, and they are not required for most layouts, but recognising them when they appear in other people's code prevents confusion, because they look unfamiliar yet follow the same logic as the physical offsets, merely expressed in writing-direction terms rather than screen-direction terms.
As with all shorthands, inset resets the offsets it covers, so writing inset: 0 after setting top: 20px in an earlier rule will override the top value to zero, because the shorthand sets all four offsets, including the ones you did not intend to change. That reset behaviour is standard, but it surprises developers who think of inset: 0 as "also set the offsets to zero" rather than "set all offsets to zero, replacing previous values". When overriding offsets in a later rule, prefer the longhand property for the edge you mean to change, and reserve the shorthand for the initial declaration where all edges are being set together. That discipline, already noted for the place shorthands in Chapter 10, applies to inset as well, and it prevents the quiet override that is hard to spot in a long stylesheet.
.backdrop {
position: fixed;
inset: 0;
}
Check: What does inset: 0 set, in terms of the four offset properties?
Section 28 — Overlapping images and layered designsOverlapping images — a photograph partially covering another, a portrait overlapping a background shape, a gallery where images break out of their grid cells to layer over each other — are a design technique that positioning and Grid overlap both enable, and the choice between them depends on whether the overlap should be structural or decorative. Grid overlap, covered in Chapter 10, places two items in the same cells, so the overlap is part of the grid's structure and moves with the grid. Absolute positioning places an image at coordinates relative to an ancestor, so the overlap is independent of the grid and can be tuned precisely — an image shifted by 40 pixels over its neighbour, rotated slightly, or offset to break the grid's edge for an editorial effect. Both produce layered designs, but absolute overlap offers finer control over the offset, while grid overlap offers stronger ties to the structure.
The classic layered image design is a hero section where a main image is overlapped by a smaller image or a shape at its corner, creating depth and visual interest. The structure is a wrapper with position: relative, containing the main image in flow and the overlapping image absolutely positioned at a corner with offsets — positive offsets to inset it, negative offsets to push it outside the wrapper's edge for a breakout effect. The overlapping image usually has a border, shadow, or background to distinguish it from the image beneath, because two photographs overlapping without separation can look like a mistake rather than a design, and the separation — a white border, a drop shadow, a solid frame — signals intentionality. The wrapper's size is determined by the main image, so the overlapping image's position is stable regardless of the main image's dimensions, as long as the offsets are chosen relative to the wrapper's edges.
Accessibility for overlapping images follows the same principles as for other positioned content: the images' document order should make sense, their alt text should describe each image's content and purpose, and decorative overlapping shapes should have empty alt text or be hidden from assistive technology, so that they do not clutter the reading experience. An overlapping image that is purely decorative — a shape, a texture, a background accent — should not be announced as content, while an overlapping photograph that carries meaning — a portrait, a product shot — should be described, regardless of its layer. The visual layering does not change the semantic structure, and the alt text should reflect the image's meaning, not its position in the stack. A layered design that is beautiful visually and coherent semantically is a design that serves every user, and that coherence comes from treating the images as content first and as layers second.
.hero-images {
position: relative;
}
.hero-images .overlap {
position: absolute;
right: -20px;
bottom: -20px;
}
Check: When should you use absolute positioning for overlapping images rather than Grid overlap?
Section 29 — Positioning with Flexbox and GridPositioned elements interact with Flexbox and Grid in specific ways that are worth stating precisely, because the interaction determines whether a positioned element participates in a flex or grid layout or escapes it. A flex item or grid item with `position: relative` remains a flex or grid item: it is arranged by its container, and then shifted from its arranged position by its offsets, without affecting the arrangement of its siblings. A flex or grid item with `position: absolute` or `fixed` leaves the arrangement entirely: it is no longer a flex or grid item for layout purposes, it does not participate in sizing, spacing, or alignment, and its container lays out its remaining items as if it were not there. A sticky item remains a flex or grid item while in flow, and sticks within its container's bounds when scrolled, as Section 9 described, with its sticking constrained by the flex or grid container's structure.
That distinction — relative and sticky stay in the layout, absolute and fixed leave it — is the key to combining positioning with the layout systems. A card in a flex row that needs a badge uses relative on the card to make it an anchor, and absolute on the badge to place it, while the card itself remains a flex item, sized and spaced by the row. A widget in a grid that needs a close button uses relative on the widget to anchor it, and absolute on the button, while the widget remains a grid item, placed by the grid. In both cases, the positioned element that leaves the layout — the badge, the button — is a child of an item that stays in the layout, so the layout's structure is undisturbed and the overlay's position is anchored to the item's box. That pattern — layout item as anchor, overlay as absolute child — is the standard way to combine the systems, and it appears in nearly every component that mixes layout and positioning.
There are also cases where a positioned element is itself a flex or grid container, or where a flex or grid container is positioned. A modal backdrop that is fixed can also be a flex container, centring its dialog, as Section 26 noted, and that combination is standard: the positioning places the backdrop, the flex layout centres its child, and the two systems operate at different levels without conflict. A grid container that is absolutely positioned can place its grid structure at a precise location, such as a dropdown panel whose internal layout is a grid, while its external position is absolute below a button. Those combinations work because positioning and layout operate on different aspects of an element: positioning determines where the element's box sits relative to its containing block, while flex or grid determines how the element's children are arranged inside that box, and the two do not interfere, because they govern different relationships — the element to its surroundings, and the element to its children.
/* A flex item can be a relative anchor for an absolute child. */
.card {
position: relative;
}
Check: Does an absolutely positioned flex item still participate in its container's flex layout?
Section 30 — Sticky versus fixed: choosing between themSticky and fixed both keep an element visible during scrolling, but they do so in different ways, and choosing between them is a decision about whether the element should permanently occupy screen space or only occupy it while its context is in view. A fixed element is always visible, always in the same place, regardless of scrolling, and it permanently reduces the visible content area by its size. A sticky element is visible in flow until its threshold is crossed, then sticks while its parent is in view, and scrolls away when its parent ends, so it occupies screen space only during its parent's passage. The fixed header is always there; the sticky section heading is there only while its section is being read. That difference in persistence is the primary criterion for choosing between them.
The secondary criteria follow from the primary one. Fixed elements need content padding to avoid obscuring, because they cover content permanently, while sticky elements need no such padding, because they start in flow and their height is already accounted for. Fixed elements are positioned relative to the viewport, so they work even when their parent is short or absent, while sticky elements need a parent taller than themselves with scrollable room, or they never stick, as Section 9 explained. Fixed elements can be placed anywhere on screen, independent of their document position, while sticky elements are constrained to their parent's bounds and their document position, so they cannot stick outside their context. And fixed elements are out of flow, so they do not affect layout, while sticky elements are in flow, so they do affect it, and their sticking is a modification of their flow position rather than a replacement for it. Each of those differences points to a different use case, and the choice should weigh them together, not just the persistence.
There is also a hybrid pattern worth knowing: a header that is sticky with a negative threshold, or a header that hides on scroll down and reappears on scroll up, implemented with JavaScript toggling a fixed or sticky class. Those patterns attempt to combine fixed's persistence with sticky's space-saving, by showing the header only when the user is likely to need it — scrolling up, or at the top of the page — and hiding it when scrolling down through content. They are more complex to implement correctly, requiring scroll-direction detection and careful handling of focus and accessibility, and they should be considered enhancements rather than defaults. The default choice remains simple: fixed for global, always-relevant chrome; sticky for contextual, parent-bound aids; and the hybrid only when user testing or a specific design requirement justifies its complexity. Starting from that simple rule and deviating only with reason produces interfaces whose scrolling behaviour feels intentional rather than accidental.
/* Fixed: always visible. Sticky: visible while its parent is in view. */
Check: What is the main difference in persistence between a fixed element and a sticky element?
Section 31 — Scroll behaviour and sticky parent boundsA sticky element's behaviour is governed by its parent's bounds in a way that deserves detailed attention, because it is the least intuitive part of sticky positioning and the source of most sticky failures. The element sticks only while its parent is in view in the relevant sense: more precisely, the element can stick only within the rectangle formed by its parent's box, offset by its threshold, so that the element's stuck position never takes it outside its parent. As the page scrolls, the parent moves up, and the sticky element moves with it until it reaches its threshold, then it stops moving — sticking — while the parent continues to move beneath it, until the parent's far edge reaches the element, at which point the element resumes moving with the parent, unsticking and scrolling away. That journey — flow, stick, unstick — is entirely determined by the parent's movement and the threshold, and sketching the parent's box and the element's path within it is the most reliable way to predict it.
A final aspect of parent bounds is their interaction with multiple sticky elements in the same parent. Two sticky headings inside one long section, both with the same threshold, will not stack neatly: the first will stick until the second reaches it, then the second will push it out, but because they share a parent, the first's unsticking is governed by the parent's bounds, not by the second's arrival, so the handover may look different from the section-by-section handover of Section 23, where each heading has its own parent. For the classic stacked-heading effect, each heading needs its own parent section, so that each heading's range is its own section and the handover is clean. That structural requirement — one parent per sticky range — is a design constraint that should shape the HTML, not just the CSS, and it is a good example of how positioning requirements can influence markup structure, in a way that flex and grid requirements rarely do. Sticky positioning is not just a CSS declaration; it is a partnership between the element, its parent, and its threshold, and all three must be designed together.
/* A sticky element needs a parent taller than itself to have room to stick. */
.article {
/* long parent */
}
.article .sidebar {
position: sticky;
top: 20px;
}
A related trap is a sticky element inside a flex or grid item whose parent is the item itself. The item's height may be only the element's height, giving no sticking room, even though the page is long. The fix is the same: make the element's immediate parent the long container, or move the element to be a direct child of it.
Another parent bounds trap involves a parent that has its own scrolling area. A sticky element inside a scrolling container sticks within that container, not within the page viewport, so its range is the scrollable height of the container rather than the page height.
Check: Why will a sticky element inside a parent exactly its own height never appear to stick?
Section 32 — A z-index scale strategyA z-index scale is a project's agreed set of z-index values, organised by layer, so that stacking decisions are made once, centrally, rather than improvised per component with ever-larger numbers. The classic failure mode is the escalating stylesheet: a dropdown at 10, a header at 20, a modal at 100, a tooltip at 1000, a new modal at 9999, and then a desperate 99999 when the new modal still appears below something — each number chosen to beat the last, without any system, until the numbers lose meaning and the stacking becomes fragile, because every new layer must guess a number larger than all previous ones, and the guess is often wrong when contexts intervene. A scale replaces that guessing with a system: layers are named, values are assigned by layer, and components use their layer's value, so that the stacking order is readable and maintainable.
A simple, effective scale uses small, spaced numbers, grouped by purpose. For example: content at auto or 0, dropdowns at 10, sticky headers at 20, fixed headers at 30, tooltips at 40, modals at 50, and notifications or toasts at 60 — each layer separated by 10, leaving room for intermediate layers to be inserted later without renumbering everything. The spacing is the point: it allows growth, so that a new layer between dropdowns and headers can take 15 without disturbing the others, and it keeps the numbers small and readable, so that a value of 30 immediately suggests "fixed header layer" to anyone who knows the scale. Large numbers like 9999, by contrast, suggest nothing except desperation, and they make it impossible to insert a layer above them without going larger still, which is how the escalation spiral begins. Small, spaced, named layers are the antidote.
The scale must also account for stacking contexts, because a scale of values only orders elements within the same context, as Section 18 stressed. A scale that assigns 50 to modals and 30 to headers works if both are in the root context, but fails if the modal is nested inside a card at z-index 1, because the card's context traps it below the header regardless of its 50. The scale therefore needs an architectural companion: a rule about where layers live — modals and toasts in a top-level layer container, dropdowns within their components but with their contexts considered, headers as top-level contexts — so that the values compete in the intended context. A scale without that architecture is a list of numbers that may not apply where they are used, and an architecture without a scale is a set of layers without an order. Together, they form a stacking system: layers placed correctly, values assigned systematically, and the whole page's stacking readable from one central definition.
:root {
--z-dropdown: 10;
--z-header: 30;
--z-modal: 50;
}
Check: Why are small, spaced z-index values better than escalating values like 9999?
Section 33 — z-index: auto`z-index: auto` is the default value, and it means the element does not create a new stacking level for itself: it participates in its parent stacking context at the level determined by its position and document order, without establishing a separate order. For a positioned element with z-index auto, the painting order is: it paints in document order among other auto elements, after flow content and before positive z-index elements, as Section 14's painting sequence described. Crucially, an element with z-index auto does not create a stacking context for its descendants — unlike a positioned element with a numeric z-index, which does — so its children's z-index values compete in the parent context, not trapped inside the element's own context. That distinction is the key to understanding auto: it is not "zero", it is "no level of my own", and its descendants are not encapsulated by it.
The difference between auto and zero is subtle but consequential, and it is worth stating precisely. An element with z-index: 0 creates a stacking context, so its descendants' z-index values are trapped inside it, and the element itself is ordered at level zero among its siblings. An element with z-index: auto does not create a context, so its descendants compete in the parent context, and the element itself is ordered by document order among auto elements, which is effectively similar to zero for its own painting but different for its children's. That means changing an element from auto to zero can break its children's stacking, by trapping them in a new context, even though the element's own painting position barely changes. It also means that setting z-index: 0 as a "harmless" way to establish stacking — a common habit — is not harmless at all: it creates a context, and that context may trap a dropdown or modal inside the element, producing the classic context bug. Auto is the safe default; zero is a deliberate context creation, and it should be used only when encapsulation is intended.
There is a related point about positioned elements with auto and their painting relative to flow content. As Section 14 noted, positioned elements paint after flow content, regardless of document order, so a positioned auto element early in the document will still paint above static content later in the document, because the painting phases differ. That means auto does not mean "paint in document order with everything" — it means "paint in document order among positioned auto elements, after all flow content". That phase distinction is why a positioned dropdown with auto still covers static content below it, even without a z-index, and why auto is often sufficient for simple overlays that are late in the document and need no explicit ordering against other positioned elements. Auto is the default for a reason: for many positioned elements, document order and painting phase together produce the correct stacking without any explicit value, and adding one only complicates the system.
.anchor {
position: relative;
z-index: auto;
}
Check: How does z-index: auto differ from z-index: 0 for an element's descendants?
Section 34 — Positioning and overflow clippingPositioned elements interact with overflow in ways that can clip them unexpectedly, and understanding that interaction is essential for dropdowns, tooltips, and other overlays that extend beyond their container's bounds. If an ancestor has `overflow: hidden`, any content extending beyond that ancestor's box — including absolutely positioned descendants that would otherwise spill outside it — is clipped at the ancestor's edge, so a dropdown inside a card with hidden overflow will be cut off at the card's boundary, even if its position would otherwise place it outside. That clipping is by design: hidden overflow means "do not show anything outside this box", and it applies to positioned descendants as well as flow content, unless the descendant is positioned relative to a different containing block outside the clipping ancestor, or the ancestor's overflow is changed. The most common dropdown bug — a menu cut off at its container's edge — is an overflow bug, not a positioning bug, and its fix is at the overflow level, not the offset level.
There is a related interaction with overflow: auto and scroll, which create scroll containers: a positioned descendant inside a scrolling ancestor scrolls with that ancestor if it is absolute, because its containing block is inside the scrolling area, but stays fixed if it is fixed to the viewport, unless a transform or other property makes the ancestor its containing block, as Section 8 noted. A dropdown inside a scrolling panel should usually scroll with the panel, which absolute positioning achieves naturally, while a modal inside a scrolling panel should usually not scroll with it, which requires placing it outside the panel or using fixed positioning with a viewport containing block. Those behaviours follow from the containing block and overflow rules together, and they should be considered when choosing where to place an overlay in the HTML: inside the scrolling area for elements that should scroll with it, outside for elements that should not.
A final overflow consideration is the viewport itself: an absolutely positioned element near the viewport's edge can overflow the viewport, producing a horizontal scrollbar or clipped content, especially on small screens. A dropdown near the right edge, a tooltip near the top, or a modal wider than a phone's screen will all overflow unless their position is adjusted — by aligning the dropdown to the right edge instead of the left, by flipping the tooltip below instead of above, or by constraining the modal's width with a maximum and internal scrolling. Those adjustments are often made with JavaScript that measures the available space, but they can also be anticipated in CSS with careful alignment choices and max-width constraints. The principle is the same as for ancestor clipping: positioned elements do not automatically avoid overflow, and their placement must account for the edges of their containing block and the viewport, or they will spill beyond them.
/* overflow: hidden on an ancestor clips absolutely positioned descendants. */
.card {
overflow: hidden;
}
Check: Why might a dropdown inside a card with overflow: hidden appear cut off?
Section 35 — Fixed elements and the viewport on mobileFixed elements on mobile devices behave differently from fixed elements on desktop, because mobile browsers have a dynamic viewport: the visible area changes as the browser's chrome — the address bar, the toolbar — shows and hides during scrolling, and as the on-screen keyboard appears and disappears. A fixed element positioned with `bottom: 0` may sit above the keyboard when it appears, or below it, depending on the browser and the viewport unit used, and a fixed header positioned with viewport-height units may jump as the address bar hides and the viewport grows. Those behaviours are not bugs in the fixed positioning model, but consequences of the viewport itself changing size, and they require mobile-specific consideration: testing on real devices, using viewport units that account for the dynamic viewport where supported, and designing fixed elements to tolerate viewport changes without jumping or obscuring content.
The viewport units themselves have evolved to address this. The classic vh unit is based on the large viewport — the size when browser chrome is hidden — in some browsers, and on the small viewport in others, producing inconsistent results for fixed elements sized with vh. Newer units — small, large, and dynamic viewport units — distinguish those cases explicitly, with the dynamic unit tracking the current viewport size as chrome shows and hides, so that a fixed element sized with dynamic units adapts smoothly rather than jumping. For learning purposes, the detail of which unit to use is less important than the principle: fixed elements sized or positioned with viewport units on mobile need testing on real devices, because emulators and desktop browsers do not reproduce the dynamic viewport faithfully, and a fixed layout that looks stable on desktop may jump on a phone. That testing is part of building fixed interfaces, not an optional extra.
Fixed elements on mobile also raise the screen-space concern from Section 22 more acutely. A fixed header and a fixed bottom bar on a phone can together consume a third or more of the screen, leaving a narrow strip for content, and that strip narrows further when the keyboard appears for text input, potentially hiding the input field behind the fixed bar or the keyboard itself. The remedies include hiding fixed bars when the keyboard is open, using sticky instead of fixed for headers on mobile, so that they scroll away and free space, and ensuring that input fields scroll into view above the keyboard, which browsers handle automatically in most cases but which fixed bars can interfere with. Those remedies should be tested with real input on a real device, because keyboard behaviour varies by browser and operating system, and a fixed layout that works with the keyboard hidden may fail when it appears.
/* Test fixed elements on a real phone: viewport size changes as chrome shows and hides. */
Check: Why can a fixed element sized with viewport units jump on a mobile browser?
Section 36 — Back-to-top buttonsA back-to-top button is a small button, usually fixed at the bottom-right of the viewport, that appears after the user has scrolled down a page and, when activated, returns them smoothly to the top. The implementation is a button with `position: fixed`, `bottom` and `right` offsets placing it in the corner, and a z-index placing it above the page content, hidden by default and shown by JavaScript when the scroll position passes a threshold — typically a few hundred pixels — so that it does not clutter the top of the page where it is unnecessary. Its action is to scroll the page to the top, smoothly if the browser and the user's motion preferences allow, and to return focus sensibly, usually to the top of the page or to the element that had focus before scrolling, so that keyboard users are not left at the bottom of the document after activating it. That combination — fixed placement, scroll-triggered visibility, smooth return, sensible focus — is the complete back-to-top pattern.
The button's placement deserves care. The bottom-right corner is the convention in left-to-right interfaces, because it is reachable, does not obscure the main reading column, and is where users expect auxiliary controls to live. But it can overlap content in that corner — a chat widget, a fixed footer bar, or a cookie banner — so its offsets should account for other fixed elements, or it should be positioned to avoid them, for example by sitting above a fixed footer bar rather than on top of it. On small screens, its size and position should be adjusted so that it does not dominate the corner or obscure text, and its hit area should remain large enough to tap comfortably, even if the button itself is small. Those adjustments are part of designing a fixed control, not afterthoughts, because a fixed control that overlaps other controls is worse than no control at all, since it blocks the very actions it sits beside.
There is also a question of whether a back-to-top button is needed at all, which should be asked before building one. On a short page, it is unnecessary clutter; on a long page with a fixed header containing navigation, users can already return to the top via the header or the browser's own controls, and a button may be redundant; on a very long page without persistent navigation — a long article, a feed, a search results list — it is genuinely useful, saving users a long manual scroll. The decision should be based on the page's length and its existing navigation, not on habit, because every fixed control occupies screen space and attention, and that cost should be justified by a real user need. When the need is real, the pattern above — fixed corner, scroll-triggered, smooth, accessible — delivers it well; when it is not, omitting the button is the better design.
.back-to-top {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 40;
}
Check: When should a back-to-top button appear, and when should it stay hidden?
Section 37 — Fixed sidebarsA fixed sidebar is a navigation or tool panel that stays visible at the side of the viewport while the main content scrolls beside it, keeping its links or controls always reachable. The implementation is a sidebar with `position: fixed`, `top` and `bottom` offsets stretching it vertically, and a `left` or `right` offset placing it at the viewport's edge, with a width set explicitly, and the main content given a margin equal to the sidebar's width plus a gap, so that the content does not slide underneath the sidebar. That margin is the fixed-sidebar equivalent of the fixed header's content padding: because the sidebar is out of flow, the content does not know it is there, and the margin is how the content makes room for it. Without that margin, the content would start at the viewport's edge and be hidden behind the sidebar, which is the most common fixed-sidebar bug.
Fixed sidebars differ from sticky sidebars in persistence and in layout cost, following the fixed-versus-sticky distinction from Section 30. A fixed sidebar is always visible, always occupying its width, regardless of scrolling, which suits global navigation that should be reachable at any point — an admin panel's menu, a documentation site's table of contents, a dashboard's tool rail. A sticky sidebar is visible only while its parent article is in view, and it occupies flow space, so no content margin is needed, which suits contextual navigation that is relevant only to its article. The choice depends on whether the sidebar's contents are global or contextual, and on whether permanently occupying a column's width is justified by the sidebar's usefulness. On wide screens, a fixed sidebar's width is a modest cost; on narrow screens, it is a large one, which is why fixed sidebars almost always collapse, hide, or become overlays on small screens, rather than remaining fixed at full width.
Responsive behaviour for fixed sidebars is usually a transformation, not just a resize: on narrow screens, the sidebar becomes a hidden drawer that slides in when a menu button is tapped, or a horizontal bar above the content, or a collapsible section, depending on the design. That transformation requires JavaScript for the drawer, or a media query for the horizontal bar, and it should be planned when the sidebar is first built, not added as an afterthought when the narrow-screen overflow is discovered. The content margin must also change at the breakpoint — removing the sidebar's margin when the sidebar is hidden, so that the content fills the width — and that margin change is part of the responsive stylesheet, coordinated with the sidebar's own change. A fixed sidebar that is designed responsively from the start, with its wide and narrow forms both considered, is a sidebar that works at every width; one that is designed only for wide screens will break at the first narrow test.
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: 240px;
}
Check: Why does main content need a margin when a sidebar is fixed?
Section 38 — Dialogs and lightboxesA dialog is a focused interaction — a form, a confirmation, a detail view — presented in a box above the page, while a lightbox is a specific kind of dialog for viewing media, usually an image or gallery, enlarged above a dimmed page. Both follow the modal pattern from Section 20: a fixed backdrop covering the viewport, a centred dialog or image within it, a high z-index in a top-level layer, and accessibility behaviours including focus management, Escape to close, and focus return. The lightbox adds media-specific behaviours: navigation between images, usually with arrow buttons and keyboard arrows, a caption describing the current image, and a close button that is clearly visible against varied image content. Those additions make a lightbox more than a modal with an image inside it; they make it a media viewer, with its own interaction model that users expect from gallery interfaces.
Dialogs and lightboxes also raise the question of the native dialog element, which modern browsers provide as a built-in dialog with much of the modal behaviour already implemented: focus management, Escape handling, backdrop styling, and top-layer placement that escapes stacking contexts automatically. Using the native dialog, where supported, is the recommended starting point for new dialogs, because it handles the accessibility and stacking details that are easy to get wrong with a hand-built fixed overlay, and its top-layer placement solves the context problem from Section 18 by design — the dialog paints in the browser's top layer, above all page content, regardless of its HTML position or its ancestors' contexts. For learning purposes, understanding the hand-built fixed pattern remains valuable, because it explains what the native dialog does and why, but for production work, the native element should be considered first, with the hand-built pattern as a fallback or a supplement for custom behaviours the native element does not provide.
There are also details about dialogs that apply regardless of implementation. A dialog's content should be focused and navigable, with a clear heading that labels its purpose, and its controls should be reachable in a logical order, without focus escaping to the page behind. A dialog that is too large for the viewport should scroll internally, so that its content remains reachable on small screens, rather than overflowing and hiding its buttons. And a dialog's backdrop click behaviour — whether clicking outside closes it — should be deliberate and communicated, as Section 20 noted, because users will click outside expecting either closure or nothing, and an unexpected behaviour in either direction feels broken. Those details, combined with the positioning and stacking foundations, turn a centred box into a dialog that users can trust, which is the standard a dialog must meet, because it interrupts the user's task and demands their attention.
.lightbox {
position: fixed;
inset: 0;
z-index: 100;
}
Check: What advantage does the native dialog element have over a hand-built fixed overlay for stacking?
Section 39 — Avoiding z-index warsA z-index war is the escalating competition of values described in Section 32: each new overlay beats the last with a larger number, until the stylesheet is full of values in the thousands and tens of thousands, none of which can be understood without tracing the entire history of additions, and each new layer risks breaking an existing one because its number was guessed rather than assigned. Wars start innocently — a dropdown at 10, a tooltip at 20, a modal at 100 — and escalate when a new component must appear above an existing one whose value is unknown or already large, so the developer picks a larger number "to be safe", which only raises the ceiling for the next component. The result is a stylesheet where z-index values convey no information except "larger than the last one", and where stacking bugs are fixed by escalation rather than by understanding, which guarantees that the next bug will be worse than the last.
The prevention is the scale and architecture from Section 32: a central set of layer values, assigned by purpose, used through variables, with overlays placed in a top-level layer so that their values compete globally. But prevention also requires discipline in the moment: when a new component needs stacking, the developer should consult the scale, assign it the next appropriate layer, and update the scale's documentation, rather than inventing a number on the spot. That discipline is a team habit, not just a technical rule, and it should be reinforced by code review: a z-index value that does not use the scale's variables, or that exceeds the scale's maximum without justification, should be questioned, because it is the first step toward a war. A scale that is documented but not enforced will be bypassed under deadline pressure, and the war will resume, so enforcement — through variables, linting, or review — is part of the system, not an optional extra.
There is also a cultural point worth making: z-index wars are a symptom of stacking being treated as an afterthought, added per component at the end of development, rather than designed as a system at the start. A project that designs its layers early — header, dropdowns, tooltips, modals, notifications, in that order, with values assigned before components are built — will rarely experience a war, because each component arrives with its layer already decided, and its value is a lookup, not a guess. A project that adds layers ad hoc, as features are requested, will experience wars, because each feature's stacking is decided in isolation, without regard to the system it joins. The lesson is to design stacking early, even for a small project, because the cost of a scale is minutes at the start and the cost of a war is hours at every subsequent feature. Stacking is a system, and systems should be designed, not accumulated.
/* Use the scale, not a guessed number. */
.new-modal {
z-index: var(--z-modal);
}
Check: What is the first step in fixing a stylesheet that already has a z-index war?
Section 40 — Positioning and accessibilityPositioning affects accessibility in ways that go beyond the visual order issues already covered, because positioned elements can be visually separated from their document position, can obscure other content, and can create interaction problems that are invisible to sighted mouse users but blocking for others. The core principle is the one from Sections 48 of Chapter 9 and 45 of Chapter 10, extended to positioning: the document order should make sense on its own, and visual positioning should present that order well, not contradict it. A badge positioned at a card's corner is read where it sits in the HTML, not where it appears visually, so its HTML position should place it near the content it annotates, not at the end of the card where it will be announced as an afterthought. A dropdown positioned below its button is read where it sits in the HTML, which should be immediately after the button, so that its items follow the button in the reading sequence, not elsewhere in the document.
Focus order is the third accessibility dimension of positioning. The tab order follows the document order, not the visual order, so a visually reordered layout — a sidebar positioned on the left but defined after the main content in the HTML, for example — will tab in document order, which may feel backwards to a keyboard user who sees the sidebar first but tabs to the main content first. That mismatch is sometimes intentional, as when main content is placed first in the document for screen readers, but it should be deliberate and tested, not accidental. A layout whose visual order and tab order disagree without reason will confuse keyboard users, who experience the page as a sequence of focus stops, and that sequence should match the visual sequence unless there is a documented accessibility reason for it to differ, such as prioritising main content for screen readers. Testing with the keyboard, tabbing through the page and noting whether the focus moves in a sensible visual sequence, is the routine that catches these mismatches.
Finally, positioned elements that appear and disappear — dropdowns, tooltips, modals, notifications — must announce their appearance and disappearance to assistive technology, or users who cannot see them will not know they exist. A dropdown that appears visually but is not announced, or a modal that opens without moving focus into it, leaves screen reader users unaware of the new content, while a tooltip that appears only on hover leaves keyboard users without the information it contains. Those announcements are typically handled with ARIA attributes, focus management, and live regions, implemented in JavaScript alongside the positioning, and they are part of building a positioned component, not a separate accessibility phase. Positioning places the component visually; accessibility makes it perceivable and operable; and a component that is only one of those is only half built.
/* Scroll padding keeps anchored headings visible below a fixed header. */
html {
scroll-padding-top: 80px;
}
Check: Why can a fixed header hide a heading that a keyboard user tabs to?
Section 41 — Transforms create stacking contextsTransforms create stacking contexts, and that side effect deserves its own section because transforms are used for so many purposes — animations, hover effects, centring, rotations — that their stacking consequence is easily overlooked until it breaks an overlay. Any transform other than none, even a translate of zero pixels or a scale of one, creates a stacking context on the element, trapping its descendants' z-index values inside it, as Section 17 listed. That means a card with a hover transform that lifts it slightly will trap its dropdown inside the card's context, so the dropdown cannot rise above a neighbouring card, even with a high z-index, unless the card's own stacking is raised. It also means a transform used for centring — the translate(-50%, -50%) technique from Section 26 — creates a context on the centred element, which is usually harmless for a modal but can affect its children's stacking if they need to escape it.
The transform-centring case is worth examining, because it is so common and its stacking effect is so often unnoticed. A modal centred with transform creates a context, and any element inside the modal that should stack above the modal's own backdrop — for example, a dropdown inside the modal that should appear above the modal's content — will have its z-index trapped inside the modal's context, which is fine as long as the dropdown only needs to rise above the modal's contents, not above elements outside the modal. But a tooltip inside a transformed card that should appear above a neighbouring card will be trapped, because the neighbouring card is outside the transformed card's context, and the tooltip's value cannot cross that boundary. The distinction is whether the overlay's target is inside or outside the transformed element's context: inside, the transform is harmless; outside, it is a trap. That inside-versus-outside test should be applied whenever a transform is added to an element containing overlays.
There is also the interaction between transforms and fixed positioning, noted in Section 8: a transformed ancestor becomes the containing block for fixed descendants, so a fixed element inside a transformed wrapper positions relative to the wrapper and scrolls with it, rather than staying fixed to the viewport. That behaviour, combined with the stacking context creation, means a transform on an ancestor changes two aspects of its descendants' positioning — their containing block and their stacking context — both as side effects of a property whose primary purpose was visual, such as an animation or a hover effect. A developer who adds a transform to a page wrapper for a subtle animation may therefore break both a fixed header inside it and a dropdown's stacking, without any change to those components' own styles, which is why transforms on large wrappers should be used cautiously, and why fixed and overlay elements are usually placed outside transformed containers.
/* Any transform creates a stacking context. */
.lifted-card {
transform: translateY(-4px);
}
Check: Why can adding a hover transform to a card break its dropdown's stacking?
Section 42 — Relative positioning for small nudgesRelative positioning's shift use — moving an element a few pixels from its normal position without disturbing the layout — is a technique for fine alignment that flow-based layout cannot always express precisely. An icon beside text that sits two pixels too high, a badge that should overlap a border by one pixel, a button label that needs a one-pixel optical adjustment, a decorative element that should sit slightly outside its container's edge — those are relative shifts, measured in single digits, applied to elements that otherwise belong in the flow. The shift is visual only, as Section 3 explained, so the element's original space remains, and neighbouring elements do not move, which is exactly what a nudge requires: a small visual correction that does not ripple through the layout. Without relative positioning, such corrections would require margins, which do affect layout, or padding, which affects the element's size, both of which would disturb the very alignment being corrected.
The distinction between a nudge and a layout change is a matter of scale and intent. A nudge is a few pixels, correcting an optical misalignment that arises from font metrics, icon shapes, or border widths — the kinds of sub-pixel visual details that make a design feel polished when correct and slightly off when not. A layout change is a structural rearrangement — moving an element to a different region, changing its column, or reordering a group — which belongs to flex, grid, or document order, not to relative shifts. Using a relative shift for a layout change produces a large gap in the flow and a large overlap elsewhere, which is fragile and confusing, while using flex or grid for a two-pixel optical correction is often impossible, because those systems align boxes, not optical centres. The two tools have different domains — relative for optical nudges, layout systems for structure — and respecting that boundary keeps both tools effective.
A caution about nudges is that they should be documented or self-evident, because a future developer who sees top: 2px on an element without context may remove it as a mysterious leftover, breaking the optical alignment it corrected. A comment explaining the nudge — "align icon optically with text" — or a class name that signals its purpose, such as icon-align, preserves the intention and prevents well-meaning cleanup from undoing it. Nudges should also be tested at different font sizes and zoom levels, because an optical correction that works at one size may not work at another, if the font metrics change — though for small, icon-related nudges, the correction usually scales acceptably. The habit to cultivate is to use nudges sparingly, for genuine optical corrections, with a clear reason, and to leave that reason visible in the code, so that the nudge is understood as a deliberate refinement rather than a random adjustment.
.icon {
position: relative;
top: 2px;
}
Check: When is a relative shift the right tool, and when should you use Flexbox or Grid instead?
Section 43 — Absolute positioning removes an element from flowAbsolute positioning's removal from flow is its defining feature, and its consequences deserve a detailed examination, because they affect layout, sizing, and accessibility in ways that are easy to overlook when focusing only on placement. When an element becomes absolute, it stops contributing to its parent's size: a parent containing only absolute children collapses to zero height, unless it has an explicit height or other in-flow content, because the absolute children are not counted in the parent's height calculation. That collapse is the most visible consequence, and it surprises developers who expect a container to grow around its absolute children, as it would around flow children. The remedy is to give the container an explicit size, or to include in-flow content that sizes it, or to reconsider whether the children should really be absolute — a container that must size to its children should contain them in flow, not absolutely.
The removal also means absolute elements do not affect their siblings' positions. A badge placed absolutely on a card does not push the card's text down or sideways; a dropdown placed absolutely below a button does not push the content below the button down; a tooltip placed absolutely above its trigger does not push the trigger's neighbours aside. That non-interference is the point for overlays, but it also means absolute elements can overlap siblings without any layout mechanism preventing it — the browser will not warn you that an absolute element covers another element's text or button, because from the layout's perspective, the absolute element is not there, and the sibling's layout proceeds as if it were absent. Overlap must therefore be managed by the developer, through offsets, z-index, and testing, rather than by the layout system, which has been explicitly told not to manage it. That management burden is the price of leaving the flow, and it should be accepted only when the element genuinely needs to be out of flow.
Accessibility consequences of flow removal follow from the document-order principle: an absolute element's reading order is its HTML order, not its visual position, so an element visually placed at the top of a card but defined at the bottom of its HTML will be read last, not first, and a dropdown visually below its button but defined before it will be read before it. That mismatch, if unintentional, confuses screen reader and keyboard users, who experience the document sequence rather than the visual arrangement. The remedy is to place absolute elements in the HTML near the content they relate to — a badge near its card's title, a dropdown immediately after its button, a tooltip immediately after its trigger — so that the document order and the visual position tell a coherent story, even though the visual position is determined by offsets and the document position by markup. Flow removal does not remove the element from the document, only from the layout, and its document position remains meaningful and should be chosen with care.
/* Absolute elements do not contribute to their parent's height. */
Check: Why does a container holding only absolutely positioned children collapse to zero height?
Section 44 — Fixed positioning removes an element from flowFixed positioning, like absolute, removes the element from flow, with the additional feature that its containing block is the viewport, so it does not scroll with the page. The flow consequences are the same as for absolute: the element does not contribute to its parent's size, does not affect its siblings' positions, and does not participate in flex or grid arrangement if it was a flex or grid item — it leaves that arrangement entirely, and its container lays out its remaining items as if it were not there. A fixed header that was once a flex item in a page column will no longer affect that column's layout, and the column's remaining items will close up around the gap it left, unless the column's content is padded to account for it, as Section 22 described for fixed headers. That gap-closing is correct behaviour for an out-of-flow element, but it must be anticipated, because a layout designed with the fixed element in flow will shift when the element is fixed.
The viewport containing block adds its own consequences. A fixed element's position is independent of its HTML position and of scrolling, so it can be defined anywhere in the document and still appear at the same viewport coordinates — though, as Section 18 noted, it should still be placed sensibly in the HTML, preferably late, so that its document order and stacking are sensible. Its size, when not set, is content-sized, like an absolute element, and opposite offsets stretch it to the viewport, so a fixed header with left and right at zero spans the viewport, and a fixed overlay with all offsets at zero fills it. And its stacking, as a positioned element, participates in the root context or in its ancestor's context if a transform or other property makes that ancestor its containing block, which is the exception from Section 8 that can make a fixed element scroll with its ancestor instead of staying viewport-fixed. Those viewport-specific behaviours are what distinguish fixed from absolute, beyond their shared flow removal.
The decision to fix an element should therefore weigh its flow removal and its viewport tie together. An element that should be always visible and always in the same place, regardless of scrolling, and whose absence from the flow can be compensated for with padding or margins, is a good fixed candidate. An element that should be visible only in context, or whose size should contribute to its parent, or whose position should scroll with its surroundings, is not, and sticky or absolute positioning, or flow layout, will serve it better. That decision, made per element rather than by habit, is what keeps fixed positioning from becoming a source of layout shifts, hidden content, and obscured interactions — the classic fixed-positioning failures, all of which trace back to an element being fixed when it should have remained in flow or been made sticky instead.
/* Fixed elements leave the flow, like absolute elements, but anchor to the viewport. */
Check: How does a fixed element's removal from flow affect its parent container?
Section 45 — Sticky positioning stays in flow until it sticksSticky positioning's defining feature is that it stays in flow until it sticks, and that duality — flow element with a fixed phase — is what makes it both powerful and subtle. While unstuck, the element occupies its normal space, contributes to its parent's height, affects its siblings' positions, and participates in flex or grid arrangement if it is a flex or grid item. Its offsets do not move it, because it has not yet reached its threshold; it sits where the flow puts it, like a relative element with no shift. When scrolling crosses its threshold, it sticks, behaving like a fixed element within its parent's bounds, but without leaving the flow in the structural sense: its original space is still reserved, in the form of its stuck position within the parent, and its siblings do not close up around it, because it never vacated its flow position in the way an absolute or fixed element does. That reservation is why sticky elements do not cause layout shifts when they stick — the space was already theirs, and sticking merely stops them from scrolling out of it.
The transition between flow and stuck is also worth understanding in terms of what changes and what does not. What changes is the element's visual position relative to the viewport: it stops scrolling and holds at its threshold. What does not change is its size, its siblings' positions, its parent's height, or its document order — all of which remain as they were in flow, because the element never left the flow. That stability is why sticky elements do not require the content padding that fixed elements do, and do not cause the layout shifts that absolute elements can cause when they appear or disappear. A dropdown that appears absolutely can overlap content that was not expecting it; a sticky element that sticks does not overlap anything it was not already beside, because it was already in that place in the flow, and sticking merely keeps it there while the rest scrolls. That predictability makes sticky the safest persistent-positioning choice, when its parent-bounds constraint can be satisfied.
There is a limit to that safety, which is the parent-bounds constraint itself: a sticky element cannot stick outside its parent, and if its parent is short, or if its parent's overflow confines it, it will not stick visibly, as Sections 9, 31, and 34 detailed. That constraint is the price of staying in flow: the element's sticking range is bounded by the flow structure it remains part of, while a fixed element's range is unbounded, because it has left the flow entirely. Choosing between sticky and fixed is therefore a choice between bounded, flow-respecting persistence and unbounded, flow-escaping persistence, and the right choice depends on whether the element's relevance is bounded by a parent — a section, an article, a table — or global to the page. When relevance is bounded, sticky's flow-respecting persistence is both sufficient and safer; when it is global, fixed's unbounded persistence is necessary, despite its greater layout cost. That bounded-versus-global distinction, more than any technical detail, is the criterion for choosing between them.
/* Sticky: in flow, then stuck within its parent. */
.sticky-item {
position: sticky;
top: 0;
}
Check: What does a sticky element keep doing, even while it is stuck, that a fixed element does not?
Section 46 — Debugging stacking with developer toolsDeveloper tools can help with stacking, though less directly than with flex or grid, because stacking is a painting order, not a layout structure, and there is no single overlay that draws the stacking order on the page. The tools that help are the element inspector, which shows the computed position and z-index of each element, the ability to inspect an element's stacking context ancestors, and, in some browsers, a dedicated layers or stacking panel that visualises the page's layers and contexts. When a stacking problem arises, the routine is to inspect the element that should be on top, check its position and z-index, then walk up its ancestors, checking each one's position, z-index, opacity, transform, filter, and other context-creating properties, until the context that traps it is found. That walk — element, then ancestors, then their contexts — is the systematic approach, and it replaces the trial-and-error escalation that wastes time.
A useful technique in the inspector is to toggle properties live, just as for flex and grid debugging. Disable a suspected context-creating property on an ancestor — an opacity, a transform, a z-index — and watch whether the element rises; if it does, that ancestor's context was the trap, and the fix is at that ancestor's level. Change the element's z-index live and watch whether it has any effect; if increasing it does nothing, the problem is a context boundary, not a value, because a value change within a context would reorder the element within that context, and its failure to move indicates that the context itself is below the target. Move the element in the document live, by cutting and pasting it in the inspector's HTML view, and watch whether its stacking changes; if moving it to the end of the document brings it forward, its document order was part of the problem, and its HTML position should be changed permanently. Those live experiments — toggling, changing, moving — test hypotheses without editing files, and they pinpoint the cause faster than reasoning alone.
The computed styles panel is essential for stacking debugging, because z-index and position values can come from multiple rules, media queries, and inherited contexts, and the authored stylesheet may not show the final resolved values. An element that appears to have no z-index in its own rule may inherit a stacking context from an ancestor's rule, or may have a z-index set by a utility class that is not obvious from the component's own styles. The computed panel shows the final values, and the inspector's display of the element's box, with its position and dimensions, confirms whether the element is where its offsets place it or whether a containing-block error has moved it. Combining the computed values with the ancestor walk gives a complete picture: the element's own stacking properties, its ancestors' context properties, and its actual rendered position, all visible in one inspection session.
/* Debug stacking by walking ancestors: position? z-index? opacity? transform? */
Check: When increasing an element's z-index has no visible effect, what does that usually indicate?
Section 47 — Common positioning mistakesThe classics are worth listing together, because recognising them by name makes them faster to fix. First, setting offsets without a position value, so the offsets are ignored — the element stays static and unmoved. Second, forgetting the relative anchor, so an absolute element anchors to a distant ancestor or the viewport instead of its intended container. Third, setting z-index on a static element, so it has no effect, and escalating the value in frustration instead of adding a position value. Fourth, trapping an overlay inside a stacking context — a card, a transformed wrapper, an opacity-faded parent — so that its high z-index cannot lift it above elements outside that context. Fifth, clipping an overlay with an ancestor's hidden overflow, so that a dropdown or tooltip is cut off at its container's edge. Sixth, making a sticky element's parent too short, so that it never has room to stick, or giving it no threshold offset, so that it never triggers. Each of these has a section in this chapter, and each is fixed by returning to the model: position scheme, containing block, context, overflow, and parent bounds.
The deepest common mistake, as in the previous chapters, is treating positioning as a set of tricks — "this snippet centres a modal", "this snippet makes a dropdown" — rather than as a model of layers, containing blocks, and stacking contexts. Developers who learn snippets without the model will be stuck whenever a layout differs from the snippets they know, because they cannot derive a new placement from first principles: which containing block, which context, which overflow, which parent bounds. Developers who learn the model can derive any placement, because the model generates the answers: choose the position scheme for the element's relationship to flow and scrolling, choose the anchor for its containing block, choose the offsets for its position within that block, choose the z-index for its layer within its context, and check the ancestors for contexts and overflow that might constrain it. That is why this chapter spent fifty sections on the model rather than presenting a cookbook of components. Components are useful, but they are outputs of understanding, not substitutes for it.
A final habit ties the mistakes together: when a positioned element looks wrong, stop and name the problem before changing anything. Is it a scheme problem, a containing-block problem, a context problem, an overflow problem, a parent-bounds problem, or an accessibility problem? Naming the category narrows the search to a handful of properties and ancestors, and the fix usually follows quickly. Randomly adjusting offsets and z-index values until the element looks right may produce a correct-looking result, but it leaves the understanding unchanged, and the same mistake will return in the next component. Name the problem, check the model, fix the cause — that routine, practised across many positioned components, is what turns a beginner who knows position properties into a developer who can build layered interfaces reliably.
/* Positioning mistakes checklist: position? anchor? context? overflow? parent? */
Check: If an absolutely positioned element appears at the page's corner instead of its card's corner, what is the most likely cause?
Section 48 — Positioning in responsive designPositioned elements in responsive design require the same attention as flow layouts, but with additional considerations, because their positions are often stated in fixed offsets that do not adapt to screen size, and their containing blocks change as layouts reflow. A badge at `top: 8px` and `right: 8px` on a card will stay in its corner at any card size, which is good, because its offsets are relative to the card's edges, not to the viewport, so it adapts automatically. But a tooltip positioned above its trigger with a fixed width may overflow a narrow viewport, where the trigger is near the edge and the tooltip's width exceeds the available space, requiring a flip to below the trigger or a width constraint at small sizes. A fixed header that spans the viewport will adapt in width automatically, but its contents may wrap and grow taller, requiring the content padding to change with it, as Section 22 noted. And a modal centred with transform will stay centred at any size, but its width must be constrained with a maximum, or it will overflow a phone's screen. The pattern is that offsets relative to a container adapt, while fixed sizes and fixed positions do not, and responsive positioning is largely a matter of preferring relative offsets and constrained sizes over fixed ones.
Media queries for positioned elements usually adjust offsets, sizes, and sometimes the position scheme itself. A sidebar that is fixed on desktop may become static or hidden on mobile, with its position value changed in a media query, so that it joins the flow or disappears into a drawer. A dropdown that drops below its button on desktop may drop above it on mobile, if the button is near the bottom of the screen, with its top offset changed to a bottom offset in a media query. A sticky heading's threshold may change on mobile to account for a different fixed header height, so that it still stops below the header rather than underneath it. Those media-query changes are the responsive work for positioned elements, and they should be planned alongside the flow layout's breakpoints, because a positioned element that is not adjusted at a breakpoint will often be the element that breaks the small-screen layout, overflowing or obscuring content that the flow layout handled correctly.
@media (max-width: 600px) {
.sidebar {
position: static;
}
}
Another mobile consideration is the on-screen keyboard. A fixed bottom bar can sit above the keyboard or be pushed off-screen depending on the browser, so test any fixed bar with an input focused, and prefer sticky or static placement for bars that must coexist with text entry on small screens.
Responsive positioning also benefits from testing with real content at the smallest width you support, not just with short placeholder text. A tooltip with a short label may fit neatly on a phone, while the same tooltip with a real sentence may overflow or wrap in a way that covers its trigger.
Check: Why might a positioned element that works on desktop overflow on a phone?
Section 49 — Choosing the right positioning toolChoosing among static, relative, absolute, fixed, and sticky should be made per element, based on its relationship to flow, scrolling, and its container, and the decision tree is worth stating explicitly. Does the element belong in the flow, arranged by its parent's layout? Then it is static, and no positioning is needed. Does it belong in the flow but need a small visual shift, or does it need to anchor absolute children? Then it is relative, with offsets for the shift or none for the anchor. Does it need to sit on top of or beside content without affecting that content's layout, anchored to a container? Then it is absolute, with offsets placing it within that container. Does it need to stay visible in the same place on screen regardless of scrolling? Then it is fixed, with viewport offsets. Does it need to stay visible while its parent is in view, then scroll away with that parent? Then it is sticky, with a threshold offset. That tree covers nearly every positioned element you will build, and walking through it for each element — flow, shift, overlay, always-visible, parent-visible — produces the correct value without guesswork.
The tree also helps avoid the most common mischoices. An element that should be sticky but is made fixed will permanently occupy screen space and require content padding, when it could have occupied flow space and stuck only in context. An element that should be absolute but is made relative with large offsets will leave a large gap in the flow and overlap content elsewhere, when it could have left the flow cleanly. An element that should be relative but is made absolute will collapse its parent's layout and lose its flow size, when it could have stayed in flow and shifted slightly. And an element that should be static but is made relative "just in case" adds an anchor and a stacking participation that may affect its children's stacking unexpectedly, when no positioning was needed at all. Each mischoice is a failure to apply the tree, and each produces a layout that works approximately but breaks under content changes, because the element's relationship to flow and scrolling does not match its actual needs.
A final habit for choosing well is to state the reason in a comment or a class name when the choice is not obvious. A relative declaration with no offsets exists as an anchor, and a comment saying "anchor for dropdown" preserves that reason. A sticky declaration with a threshold exists because its parent is long and its heading should persist, and a comment saying "sticks while section is in view" preserves that reason. A fixed declaration exists because the element is global chrome, and a comment saying "always visible header" preserves that reason. Those reasons are the design decisions behind the position values, and preserving them in the code ensures that a future developer — including yourself — can tell whether a position value is intentional or accidental, and whether changing it will break a requirement or fix a mistake. Position values without reasons are mysterious; position values with reasons are documentation, and documentation is what keeps a layered interface maintainable as it grows.
/* Choose by relationship: flow? shift? overlay? always? parent-bound? */
Check: Walk through the decision tree for a sidebar that should stay visible only while its article is on screen — which position value results?
Section 50 — Positioning and z-index in one paragraphPositioning places elements outside or shifted within the flow — static by default, relative for shifts and anchors, absolute for overlays anchored to a container, fixed for viewport-persistent chrome, sticky for parent-bound persistence during scrolling — and z-index orders those placed elements within their stacking contexts, with higher values above lower ones, document order as the fallback, and contexts encapsulating their contents so that inner values never cross a context boundary. That sentence is the whole chapter in miniature, but a summary deserves more than a sentence, because the value of this chapter is in understanding each of its clauses well enough to use them independently. The position values are the foundation: each one defines a relationship to flow, to scrolling, and to a containing block, and choosing among them is choosing that relationship, not just picking a placement technique. The containing block is the second foundation: offsets are meaningless without knowing what they are relative to, and the nearest positioned ancestor rule, with its relative-anchor idiom, is what makes absolute placement predictable rather than mysterious.
The cautions of the chapter deserve a place in the summary too, because they are what separate a working positioned interface from a good one. Visual order is not reading order, so positioned elements must be placed sensibly in the HTML as well as visually. Fixed elements obscure content, so they need padding, margins, and scroll padding to compensate. Overflow clips overlays, so overlays must live outside clipping ancestors or those ancestors must be reconsidered. Sticky elements are bounded by their parents, so their parents must be designed to give them room. And z-index values without a scale escalate into wars, so layers should be designed centrally and used through variables. Keeping those cautions in mind while you build will prevent more problems than any amount of property knowledge alone, because most positioning failures are not failures of knowing a property but failures of considering flow, context, overflow, or accessibility alongside placement.
The stacking system is the third foundation, and it repays revisiting. Z-index orders positioned elements within a context, document order breaks ties, negative values sit behind flow content but above backgrounds, auto means no level of one's own while zero creates a context, and contexts — created by positioned elements with numeric z-index, by opacity, transforms, filters, and other properties — encapsulate their contents so that only the contexts themselves are ordered against each other. Those rules together explain every stacking result, from a simple dropdown above content to a modal trapped inside a card's context.
As you finish this chapter, carry one exercise with you: build the practical task below, then deliberately break it in each of the classic ways — remove the relative anchor, set z-index on a static element, trap the modal inside a context, clip the dropdown with overflow, shorten the sticky element's parent — and fix each break by returning to the model, not by guessing numbers. That exercise will teach you more about positioning than reading this chapter twice, because it turns the model from a set of statements into a set of experiences.
Check: In your own words, what are the five position values and what does each one do?
Practical task
Build a small page that uses all three positioning techniques together. It must have a fixed header at the top with a dropdown menu that opens below its button, a long article section with a sticky subheading that sticks below the fixed header while its section scrolls, and a card with an absolutely positioned badge in its top-right corner. Add a modal dialog that opens from a button in the article, covers the page with a dimmed backdrop, and sits above the fixed header. Use a small z-index scale with variables for the header, dropdown and modal layers, and test the page by scrolling, opening the dropdown and the modal, and tabbing through it with the keyboard.
Chapter questions
- Explain the difference between absolute and fixed positioning, using the containing block of each.
- A dropdown inside a card is cut off at the card's edge. Name two possible causes and explain how you would fix each.
- A modal with z-index 1000 appears below a header with z-index 10. Explain how this can happen, and what you would change to fix it.
- Write the CSS for a badge positioned in the top-right corner of a card, and explain why the card needs position: relative.
- A sticky heading never sticks. Name three things you would check, and explain each.