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.

.card · position: relative absolute
top:0 right:0
where it would have been
relative · top:18 left:22
A relative parent anchors an absolute badge to its top-right corner. The dashed box is the relative element's original slot, preserved in flow.

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.

position: sticky · top: 0

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.

Sticky acts relative, then pins at the threshold, then releases. Scroll the box to feel it.

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:

  1. Build a card and put a small notification badge on its top-right corner. Set the card to position: relative and the badge to position: absolute; top: -6px; right: -6px. Then delete the card's relative and watch the badge jump to the page corner: you just triggered the number-one gotcha on purpose.
  2. Make a heading position: sticky; top: 0 inside a tall scrolling section and watch it pin to the top as you scroll, then release when the section ends. Remove the top value and confirm it stops sticking entirely.
  3. Stack two overlapping absolute boxes inside a relative parent. Swap their z-index values back and forth to control which one sits in front. Then set one to static and notice its z-index stops mattering.

The mental model to keep

  • static is the default; relative nudges and keeps its space; absolute leaves 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.
  • fixed pins to the viewport and ignores scroll; sticky is relative until a threshold, then sticks.
  • z-index only 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.