A sidebar with position: sticky and top: 0 follows the scroll for a while, then slides off the top of the screen. Why?
answer
- sticky has a second boundary
- it cannot leave its parent
- travel is a subtraction
- stretched items have nowhere to go
- tight wrappers kill it silently
basics
~20 sA sticky box can never be shifted outside its containing block. Once the parent's bottom edge reaches the threshold, the box stops holding position and scrolls away with the parent, so it only sticks for as long as its parent is on screen.
solid answer
~50 sSticky has two boundaries, and this is the second one biting. The element sticks inside its nearest scroll container, but it is also constrained never to leave its containing block — normally the parent's box. As you scroll, the offset the browser applies grows until the box is sitting at the parent's bottom edge; from there the constraint wins, the offset stops growing, and the box rides the parent out of view. The distance it can travel is therefore the parent's height minus the sticky element's own height. If that difference is zero the element never appears to stick at all, which is the common case when the parent is a flex or grid item stretched to exactly the child's height, or a wrapper that only contains the sticky element. The fix is to give the element more room to travel: make it a direct child of the tall column rather than of a snug wrapper, or stop it stretching with `align-self: start`.
code
css · 9 lines.column {
min-height: 100%;
}
.column > .sidebar {
position: sticky;
top: 1rem;
align-self: start;
}go deeper
Know that a sticky element can only stick while its parent is on screen, and that a parent no taller than the element gives it nowhere to travel.
Explain the constraint precisely: the sticky box is never shifted outside its containing block, so the travel distance is parent height minus element height, and a stretched flex or grid item has a travel of zero.
Diagnose it by measurement rather than by trial — compare the element's height with its real containing block's, verify the nearest parent is the one you think it is, and choose between restructuring the column and removing the stretch.
Frame it as a layout-contract decision: sticky behaviour is a property of the DOM shape, so a component that pins must document what containing block it needs. Wrapper divs added by a design system will silently disable it otherwise.
## Two boundaries, not one People usually learn the first boundary — a sticky element sticks inside its nearest scrolling ancestor — and stop there. The second boundary is what produces this symptom: a sticky box may never be shifted outside its containing block. For an in-flow element, that containing block is the box established by its parent. The sticky offset is allowed to grow only until the box reaches the containing block's far edge; after that the constraint is dropped in favour of staying inside the parent, and the element travels with the page again. That is not a bug or a browser quirk. It is what makes region-scoped sticky headers work: each section header leads its own section and hands off to the next one precisely because it cannot escape its section. ## The travel distance The useful mental model is a simple subtraction: ``` travel = containing block height - sticky element height - insets/margins in the way ``` If the parent is 2000px tall and the sidebar is 400px, the sidebar can hold `top: 0` for 1600px of scrolling and then leaves. If the parent is 400px tall and the sidebar is 400px, the travel is zero — the element is pinned and released at the same instant, which looks exactly like sticky not working. This is why the symptom "it stops sticking" and the symptom "it never sticks" have the same root cause at different magnitudes. ## Why the parent is so often exactly the child's height The usual culprit is a wrapper element added for styling that shrink-wraps around the sticky child, so parent height equals child height and travel is zero. The other frequent cause is the opposite-looking one: the sticky element is itself a flex or grid item and gets stretched. In a row-direction flex container or a grid row, items stretch to fill the cross axis by default, so the sidebar becomes exactly as tall as its containing block — again, zero travel. The element is stretched, not the parent shrunk, but the subtraction comes out the same. ```css .layout { display: grid; grid-template-columns: 240px 1fr; } .layout > aside { align-self: start; /* stop the stretch so the box is shorter than its track */ position: sticky; top: 0; } ``` With `align-self: start` the aside sizes to its content, the track is as tall as the long article next to it, and the difference is the distance the sidebar can follow. ## Diagnosing it in one pass Because the cause is geometric, the diagnosis is geometric too. Select the sticky element and its parent and compare heights. If they match, you have found it. If the parent is taller, measure the difference and compare it against how far the element actually followed before it left — they will agree, which confirms the containing block is the limit rather than something more exotic like an ancestor scroll container. A second check worth doing: confirm the parent you are measuring is really the containing block. Wrapper divs inserted by a component library sit between the element and the tall column more often than you would guess, and it is always the nearest one that constrains. ## Working with the constraint rather than against it Once you accept that a sticky element lives inside its parent, the design choices become clear. If you want the sidebar to follow for the whole page, its parent must span the whole page — put the sticky element directly inside the full-height column and let the column's height be driven by the sibling content. If you want per-section behaviour, keep tight parents deliberately: `position: sticky; top: 0` on a heading inside each `section` gives you the standard "current section header" effect, where each heading pins and is pushed out by the next one. The push-out is the same constraint doing exactly what you want. If you want the sidebar to stop short of the very bottom — for a footer, say — reduce the containing block rather than trying to override sticky. Shortening the parent, or adding bottom padding inside it, moves the release point up, because the release point is just the containing block's far edge. One more consequence worth knowing: the travel budget also depends on the element's own height, so a sidebar taller than the scrollport can appear not to stick at all even with generous parent height, because there is nothing left to scroll before the constraint releases.
- How do you make a sticky sidebar follow the scroll for the entire page instead of one section?Give it a containing block that spans the page. Put the sticky element directly inside the tall column whose height is driven by the main content, rather than inside a snug wrapper, and stop it from stretching to that column's full height — `align-self: start` in a flex or grid layout. The travel distance is column height minus sidebar height, so a short sidebar in a tall column follows almost the whole way.
- Why does this same rule make per-section sticky headings work?Because each heading is constrained to its own `section`. It pins at `top: 0` while its section is on screen, and when the section's bottom edge reaches the threshold the heading is carried out of view — visually pushed up by the next section's heading arriving. The push-out effect people build deliberately is just the containing-block constraint doing its job.
- A sticky element is taller than the scroll container. What does the user see?Effectively no sticking. The constraint can only produce visible pinning when there is scroll distance left between the element reaching its threshold and its containing block releasing it; an over-tall element consumes that budget itself. For long sticky panels the usual approach is to make the panel's inner content scroll and cap the panel height so it fits the scrollport.
saying these in an interview costs you the question
- Says sticky is broken when the parent simply ran out
- Ignores that a stretched flex or grid item has zero travel
- Thinks sticky is always scoped to the viewport height
- Adds a wrapper div and expects sticky to survive it
- Tries to fix it with a larger z-index