Flexbox and Grid arrange elements in relation to each other. The position property is for the exceptions: a badge in the corner of a card, a header that stays on screen, a tooltip over other content. Used sparingly, it is precise. Used for whole layouts, it is fragile.
The five values
| Value | In normal flow? | Offsets measured from | Typical use |
|---|---|---|---|
static (default) | Yes | Offsets do not apply | Everything, by default |
relative | Yes | Its own normal position | Containing block for children; small nudges |
absolute | No | Nearest positioned ancestor | Badges, overlays, icons |
fixed | No | The viewport | Persistent bars, floating buttons |
sticky | Yes | Its scroll container, once a threshold is reached | Sticky headers, table headings, sidebars |
The offset properties are top, right, bottom and left, or their logical versions such as inset-inline-start. They have no effect on static elements.
relative
.nudge {
position: relative;
top: 2px;
}
The element moves 2px down from where it would normally be, but the space it originally occupied stays reserved. Nothing else on the page moves. Small nudges are a minor use. The main reason to set position: relative is to make an element the reference point for absolutely positioned children.
absolute
An absolutely positioned element is removed from the normal flow. Other elements behave as if it is not there, and it is placed using offsets measured from its containing block.
For absolute, the containing block is the nearest ancestor with a position other than static. If there is none, it is the initial containing block, which is roughly the first screen of the page.
<article class="card">
<span class="badge">New</span>
<h2>Seeded rye</h2>
<p>Dense, nutty and long-keeping.</p>
</article>
.card {
position: relative; /* the badge is positioned against the card */
padding: 1.5rem;
border: 1px solid #d9e2ec;
border-radius: 0.5rem;
}
.badge {
position: absolute;
top: 0.75rem;
right: 0.75rem;
padding: 0.25rem 0.5rem;
background-color: #f0b429;
font-size: 0.875rem;
}
Without position: relative on .card, the badge would fly off to the corner of the page. That is the single most common positioning bug.
The inset shorthand
inset sets all four offsets at once, using the same one-to-four value pattern as margin:
.overlay {
position: absolute;
inset: 0; /* top, right, bottom and left all 0: cover the parent */
background-color: rgb(0 0 0 / 50%);
}
Setting opposite offsets, such as both left and right, stretches the element between them.
Sizing absolute elements
An absolute element shrinks to fit its content, like an inline-block. Give it a width, or set opposite offsets, if it needs to be a particular size.
Absolutely positioned elements do not take up space, so they can overlap text when content grows. Leave room for them (for example with padding on the parent), and test with longer text and larger font sizes.
fixed
A fixed element is positioned against the viewport and stays in place when the page scrolls:
.back-to-top {
position: fixed;
right: 1rem;
bottom: 1rem;
}
Fixed elements also leave the normal flow, so a fixed header covers the top of the page content. You must add matching space, for example padding-top on body.
There is one surprise: if any ancestor has a transform, filter or a few similar properties, it becomes the containing block for fixed descendants. The "fixed" element then scrolls with that ancestor instead of staying on screen. If a fixed element misbehaves, look up its ancestors for one of these properties.
Fixed bars take permanent screen space, which matters on small phones and when users zoom in. Keep them short, and avoid stacking several fixed bars.
sticky
A sticky element behaves like a normal element until it would scroll past a threshold, then it sticks:
.site-header {
position: sticky;
top: 0;
z-index: 10;
background-color: white;
}
As you scroll, the header moves up with the page until its top reaches 0, then it stays there. Unlike fixed, it keeps its space in the layout, so content is never hidden underneath it at the start.
A sticky sidebar
<div class="layout">
<main>Long article...</main>
<aside class="toc">Table of contents</aside>
</div>
.layout {
display: grid;
grid-template-columns: 1fr 16rem;
gap: 2rem;
align-items: start;
}
.toc {
position: sticky;
top: 1rem;
}
align-items: start matters. Without it, the grid stretches the sidebar to the full height of the row, so there is no room for it to move and it appears not to stick.
Why sticky does not stick
A sticky element sticks within its parent and within the nearest scroll container. Check these in order:
- No threshold. You must set
top,bottom,leftorright.position: stickyalone does nothing. - An ancestor has
overflowset tohidden,autoorscroll. That ancestor becomes the scroll container, and if it does not itself scroll, the element never sticks.overflow: clipdoes not cause this problem. - The parent is too short. The element cannot move outside its parent. If the parent is exactly as tall as the sticky element, there is no room to stick.
- Stretching in flex or grid. The sticky element fills its row, as in the sidebar example. Use
align-self: start.
In DevTools, select the sticky element and walk up its ancestors in the Elements panel, checking the Computed tab for overflow. A forgotten overflow-x: hidden on a wrapper is the classic cause.
Offsets and scroll position for anchored headings
With a sticky header, jumping to #section links can hide the heading under the header. Fix it with scroll-margin-top on the targets:
:target,
h2[id] {
scroll-margin-top: 5rem;
}
When not to use positioning
Avoid position: absolute for general layout, such as placing columns side by side. Absolutely positioned layouts do not adapt to different content or screen sizes and overlap as soon as anything changes. Use Flexbox or Grid, and keep positioning for small, deliberate exceptions.
Summary
relativekeeps an element in flow and makes it a containing block for absolute children.absoluteremoves an element from flow and positions it against the nearest positioned ancestor.fixedpositions against the viewport, unless an ancestor has a transform or filter.stickystays in flow until a threshold, within its parent and scroll container.- If sticky fails, check for a missing threshold, an
overflowancestor, a short parent or stretching.