A site header styled position: sticky; top: 0 renders fine but never pins when the page scrolls. How do you diagnose it?
answer
- check the inset computes first
- walk the ancestors, not the element
- non-visible overflow captures it
- hidden still scrolls, just not by you
- clip clips without capturing
basics
~20 sAlmost always an ancestor turned into a scroll container. Any ancestor with overflow hidden, auto or scroll captures the sticky element, which then sticks inside that box instead of the page — and if that box never scrolls, nothing ever happens.
solid answer
~50 sWork the causes in order of likelihood. First, confirm an inset actually computes to something other than `auto`, because sticky with no threshold is silent. Second, walk the ancestors and check computed `overflow`: any value other than `visible` — `hidden`, `auto`, `scroll` — makes that ancestor a scroll container, and the sticky element sticks inside it rather than inside the page. If that container does not itself scroll, the element pins against a scroll position that never changes, so it looks dead. `overflow: hidden` is the usual offender because it is added for reasons unrelated to scrolling, such as containing floats or clipping a decoration. Third, check the containing block has room to travel. The typical fix is to move the `overflow` rule off the ancestor, or replace it with `overflow: clip`, which clips without establishing a scroll container; `clip` is supported from Chrome 90, Firefox 81 and Safari 16.
code
css · 8 lines.page-wrapper {
overflow: clip;
}
.site-header {
position: sticky;
top: 0;
}go deeper
Know the first two checks: an inset value must be present, and an ancestor with overflow: hidden or auto will capture the element so it sticks inside that box instead of the page.
Explain why overflow: hidden counts — the box still has a scroll position, just no user scrolling — and that a non-visible value on one axis forces the other axis to auto.
Demonstrate an ordered diagnosis rather than trial and error: computed insets, then an ancestor walk reading computed overflow, then travel room; then pick the fix by asking what the overflow rule was actually for.
Push the fix upstream: an overflow: hidden sprinkled through shared layout wrappers is a standing hazard for every pinning component below it. Argue for clip or flow-root as the house default and for treating scroll-container ownership as an explicit layout decision.
## Why this is a debugging question rather than a syntax one Everything about a broken sticky element looks correct. The declaration is valid, DevTools shows it applied and winning the cascade, and the element renders exactly where you expect. Nothing anywhere reports a problem, because from the browser's point of view there is none: it is enforcing a constraint faithfully, just not against the box you had in mind. So diagnosis has to be a checklist, not an inspection of the element's own rules. ## Step 1 — does a threshold exist at all Read the computed values of `top`, `right`, `bottom` and `left`. If all four compute to `auto`, the sticky algorithm has no constraint to enforce and the element will sit in flow forever. This catches the mobile-first pattern where `position: sticky` is unconditional but the `top` lives inside a media query, and the case where a reset sets `top: auto` later in the cascade. ## Step 2 — find the real scroll container A sticky element sticks inside its nearest **scroll container**, which is the nearest ancestor whose computed `overflow` on either axis is something other than `visible`. Crucially that includes `hidden`. A box with `overflow: hidden` has a scrollable overflow region and a scroll position — the user just cannot move it. So a sticky descendant is scoped to that box, its scroll position never changes, and the threshold is never crossed. This is the single most common cause, and it is common precisely because `overflow: hidden` is written for reasons that have nothing to do with scrolling: clipping a background flourish, containing a float, hiding a horizontal overflow caused by something else, or as part of a card component several layers up. `overflow: auto` on a wrapper does the same thing whenever the wrapper is not the box the user actually scrolls. ```css /* Somewhere up the tree, added to hide a decorative overflow */ .page-wrapper { overflow: hidden; } /* Now this sticks inside .page-wrapper, which never scrolls */ .site-header { position: sticky; top: 0; } ``` To find it, walk from the sticky element to the root and read each ancestor's computed `overflow-x` and `overflow-y` in the Computed pane. Filter for `overflow` and step up one element at a time — the first ancestor that is not `visible` is the box your element is sticking inside. Remember that setting a non-`visible` value on one axis forces the other axis to compute to `auto` when it was `visible`, so `overflow-x: hidden` alone is enough to create a scroll container in both directions. That surprises people who assumed a horizontal-only rule was harmless. ## Step 3 — is there travel room If the scroll container is correct, compare the sticky element's height with its containing block's. If they match, the element pins and releases at the same instant. Default stretch alignment on a flex or grid item is the usual reason those two heights are equal. ## The fixes, in preference order **Remove the `overflow` rule.** Ask what it was for. Often it was containing floats, in which case `display: flow-root` does that job without creating a scroll container. Often it was hiding an overflow whose real cause is a too-wide child, which is better fixed at the child. **Replace `hidden` with `clip`.** `overflow: clip` clips painting at the box's edges but does not establish a scroll container, so sticky descendants keep sticking against the page. It is the direct answer when clipping is genuinely what you wanted. Support is broad in evergreen browsers — Chrome 90, Firefox 81, Safari 16 — but if you must support older engines, guard it with `@supports (overflow: clip)`. **Move the `overflow` off the sticky ancestry.** If the clipping applies to a sibling region rather than to the header's chain, restructure so the rule does not sit above the sticky element. **Accept the container and scroll it.** Sometimes the ancestor really is meant to scroll — a panel with `overflow: auto`. Then sticky works fine relative to that panel, and the mistake was expecting the element to react to page scroll instead. ## Things that are not the cause A few reflexes are worth unlearning. `z-index` never affects whether an element sticks; it only affects whether the stuck element paints above the content passing under it. `display: flex` or `display: grid` on an ancestor does not create a scroll container. And `position: sticky` needs no JavaScript to work — reaching for a scroll listener at this point means the actual constraint has not been found yet.
- Why does overflow: hidden break sticky when it shows no scrollbar at all?Because a scroll container is defined by having a scrollable overflow region and a scroll position, not by having a visible scrollbar. `overflow: hidden` gives the box both — it is scrollable programmatically, just not by the user. A sticky descendant is therefore scoped to that box, and since its scroll position never changes, the threshold is never crossed.
- You need to clip an ancestor's overflow but keep sticky working. What do you use?`overflow: clip`. It clips painting at the box edges without establishing a scroll container, so sticky descendants continue to stick against the page scrollport. It is available in Chrome 90+, Firefox 81+ and Safari 16+; if you need a fallback, put it behind `@supports (overflow: clip)` with `hidden` as the degraded path. Where the rule was really there to contain floats, `display: flow-root` is the better replacement.
- Does overflow-x: hidden alone create the problem, or do you need both axes?One axis is enough. When one axis computes to a non-`visible` value and the other was `visible`, that other axis computes to `auto`, so the box becomes a scroll container in both directions. A rule written to stop horizontal overflow therefore captures vertical sticky descendants too, which is why this cause is so easy to miss in review.
saying these in an interview costs you the question
- Reaches for a scroll listener before checking ancestors
- Thinks overflow: hidden cannot affect scrolling behaviour
- Raises z-index to make an element stick
- Checks only the element's own rules, never its ancestors
- Assumes display: flex on a parent breaks sticky