Skip to content
A
Ada Lovelace
Alan Turing
Anita Borg
B
Beatrice Worsley
Bernard Galler
Bert Sutherland
Betty Holberton
Bob Bemer
C
Carl Petri
Charles Babbage
Christopher Strachey
Claude Shannon
Corrado Böhm
D
Danny Cohen
David Wheeler
Dennis Ritchie
Donald Davies
Dorothy Vaughan
Douglas Engelbart
E
Edgar Codd
Edmund Berkeley
Edsger Dijkstra
Evelyn Berezin
Shadow when stuck

Scroll

Sticky

An element scrolls with the page until it hits an edge, then stays pinned while the rest of its section scrolls underneath.

Sticky is CSS position: sticky. The element sits in the normal flow until scrolling brings it to the offset you set, then it holds there until the end of its parent pushes it out. That last part is why section headers hand off to each other: each header only sticks inside its own section.

Also called sticky header, sticky section header, pinned header, header that stays at the top, position sticky, sticky table header, stuck header, header stays while scrolling, content scrolls under the header, sticky nav, section headers push each other

Use when

  • Section headers in long lists, like contacts, settings or a changelog.
  • Table headers that need to stay visible while rows scroll.
  • A toolbar that should stay within reach while its content scrolls.

Avoid when

  • Small screens where a pinned element takes a big share of the viewport.
  • Stacking several sticky bars, which leaves little room to read.
  • When the element must stay visible past the end of its parent. Sticky cannot leave its container, so use a fixed element instead.

Prompt

Make the section headers in this list sticky.

- Wrap each header and its rows in one element, header first.
- Header: real h2 or h3, `position: sticky; top: 0px;` in the list's own scroll container, opaque background, z-index above rows.
- Headers are 32px tall; `scroll-padding-top: 32px` on the scroll container.
- No `overflow: hidden`, `auto` or `scroll` on ancestors between header and scroll container; use `overflow: clip`.
- Scroll container: no padding. First section wrapper: `margin-top: 1px`.
- A last section shorter than the container never pins (expected; don't pad it).
- Header: `transition: box-shadow 150ms ease` (none under reduced motion). `[data-stuck="true"]`: `box-shadow: var(--stuck-shadow, 0 1px 0 var(--border, rgb(0 0 0 / 0.08)), 0 4px 12px rgb(0 0 0 / 0.06))`.
- Dark (`prefers-color-scheme: dark`): `--stuck-shadow: 0 1px 0 rgb(255 255 255 / 0.12), 0 4px 12px rgb(0 0 0 / 0.4)`.
- One IntersectionObserver on the headers: root = scroll container, rootMargin '-1px 0px 100000px 0px', threshold [0, 0.01, 1]. Stuck = `entry.isIntersecting && entry.intersectionRatio < 1 && entry.boundingClientRect.top <= entry.rootBounds.top && entry.boundingClientRect.bottom > entry.rootBounds.top`, written to the header as `data-stuck="true"` or `"false"`. Use these values exactly.

Acceptance checklist

  • While scrolling, each header pins to the top of the scroll container (a last section shorter than the container never pins; that is expected).
  • The next header pushes the current one up; they never overlap.
  • No row content is visible through a stuck header.
  • The shadow appears only while a header is stuck, also after setting the scroll container's scrollTop past a header in one step, and goes away when the header scrolls back into place or once it is pushed out (a 1px sliver may remain).
  • Headers pin to the top of the list's own scroll area, not to the page.
  • Tabbing through rows, including Shift+Tab back up, never leaves the focused row hidden behind a stuck header.
  • Screen readers read each header before its rows.

Code

.list {
  overflow-y: auto;
  padding: 0; /* padding goes on a wrapper, so rootMargin lines up with the top */
  scroll-padding-top: 32px; /* header height */
}

.section-header {
  position: sticky;
  top: 0px;
  z-index: 1;
  min-height: 32px;
  background: var(--surface, #fff); /* opaque, so rows never show through */
  transition: box-shadow 150ms ease;
}

.section-header[data-stuck="true"] {
  box-shadow: var(--stuck-shadow, 0 1px 0 var(--border, rgb(0 0 0 / 0.08)), 0 4px 12px rgb(0 0 0 / 0.06));
}

/* Tune per theme: the light shadow vanishes on a dark surface. */
@media (prefers-color-scheme: dark) {
  .list { --stuck-shadow: 0 1px 0 rgb(255 255 255 / 0.12), 0 4px 12px rgb(0 0 0 / 0.4); }
}

/* The first header starts 1px below the stuck line, so it is not flagged stuck at rest. */
.list > section:first-of-type {
  margin-top: 1px;
}

@media (prefers-reduced-motion: reduce) {
  .section-header { transition: none; }
}

Accessibility

Keep stuck headers as real headings in reading order, and set scroll-padding-top on the scroll container so a focused row is never hidden underneath a stuck header.

  • Scroll FadeSoon

    A soft fade on the edge of a scroll area that still has more content, hinting there is more to see.

  • Scroll Snap

    Scrolling comes to rest exactly on an item instead of stopping anywhere in between.

  • Pinned Horizontal ScrollSoon

    A section pins in place while vertical scrolling moves its content sideways.

Often confused with

  • Pinned Horizontal ScrollSoonSticky pins one element while its section scrolls past. Pinned horizontal scroll pins a whole section and turns vertical scrolling into sideways movement.