Articles /

layout

Sticky positioning: the complete guide

A hybrid of relative and fixed positioning that sticks an element within its scrolling container. Powerful once you understand the rules.

How sticky positioning works

An element with position: sticky behaves like position: relative until the viewport scrolls past a specified threshold, at which point it becomes fixed relative to its nearest scrollport. You must set at least one inset property (top, bottom, left, or right) for sticky to activate.

.sticky-header {
  position: sticky;
  top: 0;
  background: oklch(0.98 0 0);
  z-index: 10;
}

The element remains in the normal document flow — it still occupies its original space, unlike position: fixed which removes it entirely.

The containing block constraint

A sticky element can only stick within the bounds of its containing block — typically its parent. Once the parent scrolls out of view, the sticky child goes with it. This is the most misunderstood aspect of sticky positioning.

/* The sidebar sticks only while .layout is in view */
.layout {
  display: grid;
  grid-template-columns: 1fr 280px;
  align-items: start;
}

.layout__sidebar {
  position: sticky;
  top: 2rem;
  /* Sticks until .layout scrolls past */
}
If your sticky element isn't sticking, check that its parent is tall enough. A sticky child inside a parent with no extra height has nowhere to stick.

Scroll-margin and scroll-padding

When you have a sticky header, in-page anchor links will scroll content behind it. Use scroll-margin-top on target elements or scroll-padding-top on the scroll container to compensate:

/* Option 1: on the scroll container */
html {
  scroll-padding-top: 4rem; /* height of sticky header */
}

/* Option 2: on individual targets */
[id] {
  scroll-margin-top: 4rem;
}

Sticky sidebar pattern

A common pattern pairs a long content area with a sticky sidebar. The sidebar stays visible while the user scrolls through the main content:

.article-layout {
  display: grid;
  grid-template-columns: 1fr 16rem;
  gap: 2rem;
  align-items: start; /* critical — prevents stretching */
}

.sidebar--sticky {
  position: sticky;
  top: 2rem;
  max-height: calc(100vh - 4rem);
  overflow-y: auto;
}

The align-items: start on the grid container is essential. Without it, the sidebar stretches to match the content column's height, eliminating the scroll distance sticky needs.

Common gotchas

  • overflow: hidden on an ancestor — any ancestor with overflow: hidden, overflow: auto, or overflow: scroll becomes the scroll container, breaking sticky relative to the viewport.
  • Missing inset property — without top, bottom, left, or right, the element will never stick.
  • Flexbox stretch — a flex child stretches to fill its cross axis by default. Use align-self: start to prevent it.
  • No room to scroll — the sticky element's parent must be taller than the sticky element itself for sticking to occur.
Use DevTools' "Show scroll snap areas" or the Elements panel badge for position: sticky to debug sticky issues visually.

Stacking with z-index

Sticky elements naturally create a stacking context when combined with z-index. For sticky headers, always set an explicit z-index and a background color — otherwise content will visibly scroll behind the transparent header:

.page-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: oklch(0.15 0.02 260);
  box-shadow: 0 1px 0 oklch(0.5 0 0 / 0.15);
}