Relative and absolute positioning in CSS work as a pair: the relative box keeps its place in the layout and becomes the anchor, while the absolute child is lifted out of that flow and placed against that anchor.
What does position: relative actually change?
A relatively positioned element stays exactly where the normal flow put it. The document still reserves the space it occupies, the elements after it still begin below it, and nothing else on the page reflows. What changes is that the box can now be nudged with offsets, and that it becomes the containing block for any absolutely positioned descendant.
That second effect is why the two values get used together so often. Give a wrapper position: relative and the children inside it are free to use position: absolute with top and left measured from that wrapper's padding box, no matter how far down the page the wrapper happens to sit.
How does an absolutely positioned element find its anchor?
Absolute positioning removes the box from the flow completely. The box is sized and placed against its containing block, and the containing block comes from a single rule: use the nearest ancestor whose position is not static.
Two details catch people out. First, when no ancestor is positioned, the box falls back to the initial containing block, a viewport-sized rectangle at the top-left of the document. That is why an unanchored dropdown appears jammed into the corner of the page. Second, a handful of properties create a containing block even without position: a non-none transform, a filter, a perspective, contain: layout or contain: paint, and will-change naming any of those. An animated wrapper is the usual accidental culprit, because the animation applies a transform that quietly becomes the new anchor.
<figure class="diagram">
<img src="trainer-diagram.jpg" alt="Side view of a running shoe with three marked parts">
<button class="hotspot spot-1" type="button" aria-label="Part one, the collar">1</button>
<button class="hotspot spot-2" type="button" aria-label="Part two, the midsole">2</button>
<button class="hotspot spot-centre" type="button" aria-label="Part three, the lace panel">3</button>
<figcaption class="diagram-note">Select a marker to inspect a part</figcaption>
</figure>
.diagram {
position: relative;
margin: 0;
overflow: hidden;
}
.diagram img {
display: block;
width: 100%;
height: auto;
}
.hotspot {
position: absolute;
width: 2rem;
height: 2rem;
border: 2px solid #ffffff;
border-radius: 50%;
background: #1d4ed8;
color: #ffffff;
}
.spot-1 { left: 38%; top: 22%; }
.spot-2 { right: 12%; bottom: 18%; }
.spot-centre { left: 50%; top: 50%; transform: translate(-50%, -50%); }
Read those two rules in order and you can predict the result without guessing. The .diagram rule sets position: relative and nothing competes with it, so the figure is the anchor. The .hotspot rule sets position: absolute, so all three markers leave the flow and are placed against the figure instead of against the image. The marker that has no left or right keeps the static position it would have had, which is why the second marker is pinned with right and bottom instead of left and top.
Why do percentage offsets resolve against the anchor?
An offset percentage on an absolutely positioned box is measured against the size of its containing block. left: 50% therefore means half the width of the anchor away from the anchor's left padding edge, not half of the element's own width and certainly not half of the screen. That is what makes the pin-to-the-middle trick work, and the .spot-centre rule shows the full version of it.
In that rule the offsets move the marker's top-left corner to the centre of the figure, and the transform then pulls the marker back by half of its own width and height. Translate percentages are measured against the element's own box, so the correction is exact whatever size the marker ends up being. Delete the transform and the marker sits with its corner on the centre point instead of its middle, which is a good way to see what the transform is doing.
Two further rules are worth memorising. If top and bottom are both auto, the box uses its static position, the spot it would have occupied in the flow. And if left, right and width are all given non-auto values at once, the result is over-constrained: in left-to-right writing the right value is ignored and the margin values absorb the difference. The logical equivalents, inset-inline-start and inset-block-start, do the same job for a page that mixes writing directions, and they are supported in current evergreen browsers.
How do you build a panel that hangs off a field?
The dropdown that opens under a search field is the pattern worth mastering, because it uses every rule above at once. The field wrapper is the anchor, the panel is absolutely positioned and out of the flow so it does not shove the rest of the form down, and the offsets are lengths or percentages measured from the wrapper.
<div class="field">
<label for="city">City</label>
<input id="city" name="city" type="text" autocomplete="address-level2">
<ul class="suggestions">
<li><button type="button">Lisbon</button></li>
<li><button type="button">Luanda</button></li>
</ul>
</div>
.field {
position: relative;
display: grid;
gap: 0.25rem;
}
.suggestions {
position: absolute;
inset: calc(100% + 0.25rem) 0 auto 0;
width: max-content;
max-width: 20rem;
margin: 0;
padding: 0.25rem;
list-style: none;
background: #ffffff;
border: 1px solid #d7dee7;
border-radius: 8px;
}
inset is shorthand for the four physical offsets, so inset: calc(100% + 0.25rem) 0 auto 0 reads as top, right, bottom, left. The top value is the wrapper's full height plus a small gap, so the panel starts below the input, and the bottom offset is auto so the panel takes the height its content needs. The left and right offsets are both zero, which would stretch the panel across the wrapper, but a width is given as well, so the box is over-constrained: in left-to-right text the right offset is ignored and the panel sits at the left edge of the wrapper at its content width, capped at twenty rem.
Where does overflow clipping come in?
Clipping follows the containing block rather than the document tree. An ancestor with overflow: hidden clips an absolutely positioned descendant only when that ancestor sits in the descendant's containing block chain, which in practice means the ancestor must be the anchor or contain it. A hidden wrapper that is not positioned will happily let a panel escape, and a panel that escapes a scrolling ancestor is a common reason content gets cut off in one direction only.
| Situation | Anchor used | Result |
|---|---|---|
| Absolute child, positioned parent | The positioned parent | Child sits at the parent's padding box |
| Absolute child, no positioned parent | Initial containing block | Child lands at the top-left of the document |
| Absolute child, transformed grandparent | The transformed element | Offsets ignore the real parent |
| Relative child inside a positioned parent | Its own normal spot | Space is preserved, box is offset |
Rules worth keeping
- Position the parent
relativewhenever you position a childabsolute. - Relative keeps its space; absolute keeps nothing but its own box.
- Offsets are measured from the containing block's padding box, not from the viewport.
- Percentages on offsets resolve against the anchor's size.
- A transform, filter or contain value on an ancestor makes that ancestor the anchor.
- Over-constrained boxes ignore the end-side offset in left-to-right text.
Try it yourself: build the shoe-diagram page above, then delete the transform from the .spot-centre rule and watch the third marker jump so that its corner lands on the centre of the picture. Put the transform back and delete position: relative from .diagram instead, and note where all three markers travel to. Finally, widen the diagram to 40rem and confirm that the percentage offsets keep the markers in the same proportion of the picture.