Learn CSS · Lesson 5 of 7 · 8 min
Positioning
By default, boxes line up one after another in the order you wrote them. The
position property is how you take a box out of that line and place it
somewhere of your choosing: nudged a little, pinned to a corner, or stuck to the top of the screen as you
scroll. Five values do all of it, and most confusion comes from not knowing which one to reach for.
Normal flow is the starting point. Block elements stack top to bottom, inline elements sit left to right,
and nobody overlaps. The position property changes the rules of that flow. It takes five
values, and they fall into two groups: static and relative stay in the
flow, while absolute and fixed leave it.
sticky is the clever hybrid that does both.
static: the default, normal flow
Every element starts as position: static. It sits exactly where normal flow puts it, and the
offset properties (top, right, bottom, left) and
z-index are simply ignored. You almost never type static
yourself; it is the value you change away from when you want a box to move.
.thing {
position: static; /* the default for everything */
top: 40px; /* ignored: static elements don't move */
} The takeaway: if you set top or left on an element and nothing happens, the most
common reason is that the element is still static. Offsets only do something once you pick one
of the four positioning values below.
relative: nudge from where it would have been
Setting position: relative keeps the element in the flow, in its original spot, but now
top, right, bottom and left shift it visually
from that spot. Crucially, the space it originally occupied is preserved:
neighbours do not move to fill the gap, so the element can appear to overlap them.
.badge {
position: relative;
top: 8px; /* push 8px down from its natural top */
left: 12px; /* push 12px right from its natural left */
}
/* the original slot is left empty; nothing reflows */ There is a second, far more important reason to reach for relative, and it is the key to the
whole lesson: a relatively positioned element becomes a positioning context for any
absolute children inside it. More on that in a moment. In practice, you often set
position: relative on a box without any offsets at all, purely to anchor something
absolute inside it.
absolute: removed from flow, pinned to an ancestor
position: absolute takes the element completely out of normal flow. The
space it used to occupy collapses, neighbours close up as if it were never there, and the element is now
free to float anywhere. Its top/right/bottom/left values
are measured from its nearest positioned ancestor, meaning the closest parent (or
grandparent) that has a position other than static.
And here is the single most common position gotcha, the one that trips up every beginner:
if no ancestor is positioned, an absolute element anchors to the page itself (the initial
containing block) and flies off to the corner of the whole document instead of the box you expected.
/* THE FIX you will reach for constantly: */
.card {
position: relative; /* make THIS the anchor */
}
.card .badge {
position: absolute;
top: 10px;
right: 10px; /* 10px from the card's corner, not the page's */
} Read that pattern until it is muscle memory: relative parent, absolute child. It powers
notification badges on icons, "New" ribbons on cards, close buttons in the corner of a modal, and dropdown
menus that hang off a button. Whenever you want to pin something to the corner of a specific box, you set
that box to relative and the thing to absolute.
The positioning playground (live)
Below is the relative parent, absolute child pattern in action. The outer card is
position: relative. The dashed outline shows where the inner "relative" box would have sat in
normal flow; the solid box is the same element nudged down and right with top and
left, with its original slot left untouched. The pill in the corner is
position: absolute, pinned to the card because the card is its positioned ancestor.
top:0 right:0
Notice three things at once: the badge clings to the card's corner (not the page), the relative box moved
but its empty original footprint stayed put, and nothing else reflowed. That is the difference between
relative (keeps its space) and absolute (gives its space up) in one picture.
fixed: pinned to the viewport
position: fixed is like absolute, with one change: it is positioned relative to
the viewport (the browser window), not to any ancestor, and it does not move when
the page scrolls. Set top/left and it stays glued to that spot no matter
how far down the reader goes.
/* a header that stays put while you scroll */
.site-header {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100; /* sit above the scrolling content */
}
/* the classic back-to-top button */
.to-top {
position: fixed;
bottom: 24px;
right: 24px;
} This is the tool behind sticky-looking headers, floating action buttons, cookie bars and back-to-top
buttons. One caution: because a fixed element leaves normal flow, the content underneath can slide behind
it. A fixed header usually needs a matching padding-top (or margin) on the page body so the
first paragraph is not hidden beneath it.
sticky: relative until it hits a threshold
position: sticky is the best of both worlds and the newest of the five. The element behaves
like relative (it scrolls along normally, keeping its space) until it reaches a scroll
threshold you set, and from that point it sticks in place like fixed while its
container is still on screen. It needs at least one offset, almost always top, to know when to
stick.
.section-heading {
position: sticky;
top: 0; /* stick once it reaches the top edge */
}
/* scrolls with the page, then pins at the top, then releases
when its parent section scrolls away */ Sticky is perfect for section headings that pin while you read their section, a table header that stays
visible, or the lesson sidebar to the left of this very page. Two things commonly break it: the offset
(sticky with no top/bottom simply never sticks), and a parent with
overflow: hidden or a fixed height, which clips the sticky behaviour. Here it is, live: scroll
this little box and watch the label pin to its top.
Scroll inside this box. The bar above scrolls with the content at first, then sticks to the top edge once it gets there, then releases when this scroll area runs out.
Keep scrolling. The sticky bar stays pinned for as long as its container is still in view, exactly how this page's lesson sidebar behaves on a wide screen.
When you reach the bottom, the bar would let go and scroll away with the rest. That is the whole behaviour: relative, then fixed-like, then relative again.
z-index and stacking
Once boxes can overlap, you need a way to say which one wins the overlap. That is z-index: a
higher number sits in front of a lower one. The catch that surprises everyone:
z-index only works on positioned elements, that is, anything that is
relative, absolute, fixed or sticky. On a plain
static element it does nothing at all.
.modal-backdrop { position: fixed; z-index: 200; }
.modal { position: fixed; z-index: 201; } /* above the backdrop */
.tooltip { position: absolute; z-index: 10; } One more subtlety worth knowing early: every positioned element with a z-index creates a new
stacking context, a self-contained layer. A child's z-index: 9999 can never
escape above a sibling of its parent if the parent sits lower in the stack. When a "huge z-index" stubbornly
refuses to come to the front, a trapping stacking context on an ancestor is usually the reason, not the
number you chose.
When NOT to reach for positioning
Here is the rule that saves the most grief: positioning is for putting one element on top of, or
pinned to, another. It is not for general page layout. If you find yourself nudging boxes around
with relative and offsets to line up columns or space out a row, you are using the wrong tool,
and the result will be fragile the moment text length or screen size changes.
For laying elements out next to each other, use flexbox for one direction
(a row or a column) and grid for two dimensions (rows and columns together).
They handle alignment, spacing and wrapping for you, and they respond to content instead of relying on
magic pixel numbers. Save position for the things it is uniquely good at: badges, overlays,
dropdowns, sticky headers and tooltips.
Practice
Two minutes each, and each one drills a single idea. Use a blank CodePen, your browser's DevTools, or one of our tools to experiment:
- Build a card and put a small notification badge on its top-right corner. Set the card to
position: relativeand the badge toposition: absolute; top: -6px; right: -6px. Then delete the card'srelativeand watch the badge jump to the page corner: you just triggered the number-one gotcha on purpose. - Make a heading
position: sticky; top: 0inside a tall scrolling section and watch it pin to the top as you scroll, then release when the section ends. Remove thetopvalue and confirm it stops sticking entirely. - Stack two overlapping
absoluteboxes inside arelativeparent. Swap theirz-indexvalues back and forth to control which one sits in front. Then set one tostaticand notice itsz-indexstops mattering.
The mental model to keep
staticis the default;relativenudges and keeps its space;absoluteleaves flow and pins to the nearest positioned ancestor.- The pattern you will use most: relative parent, absolute child to pin something to a box's corner.
fixedpins to the viewport and ignores scroll;stickyis relative until a threshold, then sticks.z-indexonly affects positioned elements, and stacking contexts can trap it.- For real layout, reach for flexbox and grid, not positioning.
That is positioning. It pairs naturally with the box model you just learned, since every box you position is still made of content, padding, border and margin. Next up: laying those boxes out in rows and columns.