position: sticky feels like it should be simple.
You want a header, sidebar, table heading, or little piece of navigation to stick while the page scrolls. You add:
.sidebar {
position: sticky;
top: 1rem;
}And sometimes it works perfectly.
Other times, nothing happens.
The element scrolls away like normal. Or it sticks for a second and then disappears. Or it sticks inside the wrong part of the page. Or it works until you add one innocent-looking wrapper.
The rule is not broken. The mental model is usually incomplete.
position: sticky is not just about the element itself. It depends on the element, its offset, its parent, its scrolling ancestors, and the amount of space available for it to move.
That is why sticky bugs can feel so mysterious. For the broader set of layout mental models, start with the CSS Survival Guide: The 20% That Solves 80% of Layout Problems.
Sticky is part relative, part fixed
A sticky element starts out like a relatively positioned element. It sits in the normal layout where it would naturally appear.
Then, when the scroll position reaches a threshold, it behaves more like a fixed element within a specific boundary.
That threshold usually comes from an offset:
.sticky {
position: sticky;
top: 1rem;
}This means:
Act normal until you are 1rem from the top of the scrolling area. Then stick there.Without an offset, the browser does not know when the element should become sticky.
This is one of the simplest sticky bugs:
.sticky {
position: sticky;
}That is incomplete.
You usually need at least one of these:
.sticky {
position: sticky;
top: 0;
}Or:
.sticky {
position: sticky;
bottom: 0;
}Or, for horizontal layouts:
.sticky {
position: sticky;
left: 0;
}The offset is the trigger.
Sticky sticks inside a scrolling area
The biggest source of confusion is this: sticky positioning is tied to a scrolling area.
A sticky element does not always stick relative to the viewport. It sticks relative to its nearest relevant scrolling ancestor.
That matters because a parent can become a scrolling ancestor without you thinking of it that way.
For example:
.wrapper {
overflow: auto;
}
.sidebar {
position: sticky;
top: 1rem;
}Now the sidebar may stick inside .wrapper, not the page.
That might be what you want. But if you expected the sidebar to stick while the whole page scrolls, this can feel broken.
The same problem can happen with:
.wrapper {
overflow: hidden;
}Or:
.wrapper {
overflow: scroll;
}Or sometimes with one-axis overflow rules like:
.wrapper {
overflow-x: hidden;
}Overflow is not just visual clipping. It can change the scroll context that sticky positioning cares about. For a deeper look at that behavior, read The Hidden Complexity of CSS Overflow.
When sticky fails, inspect the ancestors first.
The bug is often above the sticky element, not on it.
overflow: hidden is a common sticky killer
This pattern appears all the time:
.page-wrapper {
overflow: hidden;
}Maybe it was added to hide a decorative blob. Maybe it was added to remove a horizontal scrollbar. Maybe it was added to clip a border radius.
Then later, someone adds a sticky sidebar inside the wrapper:
.sidebar {
position: sticky;
top: 2rem;
}And the sidebar does not behave as expected.
The sticky rule looks correct. The offset is there. The sidebar is in the right place.
But the wrapper changed the rules.
A better fix is often to move the clipping to a smaller element:
.hero-art {
overflow: hidden;
}Instead of clipping the whole page wrapper, clip only the thing that actually needs clipping.
This is the same general rule as overflow debugging: use overflow at the smallest level that solves the problem.
The parent needs enough height
A sticky element can only stick within the bounds of its containing block.
If the parent is too short, there is no room for the sticky behavior to happen.
For example:
<section class="intro">
<aside class="note">Sticky note</aside>
</section>.note {
position: sticky;
top: 1rem;
}If .intro is only slightly taller than .note, the note has nowhere to stick. It reaches the boundary of its parent almost immediately.
This is common in layouts where the sticky element is inside a small wrapper instead of the larger page section.
The fix is not always CSS on the sticky element. Sometimes the markup needs to change so the sticky item lives inside the region where it should remain sticky.
For example, a sticky sidebar usually wants to be inside a layout container that spans the full article area:
<div class="article-layout">
<aside class="toc">...</aside>
<article>...</article>
</div>.article-layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 2rem;
}
.toc {
position: sticky;
top: 2rem;
align-self: start;
}The sidebar can now stick while the article content scrolls past it.
Sticky elements should often use align-self: start
Sticky sidebars inside grid or flex layouts can hit another subtle issue.
Grid and flex items may stretch by default.
If the sticky element is stretched to match the height of the row or container, it may not have the movement you expect.
This is a common grid layout:
.layout {
display: grid;
grid-template-columns: 240px 1fr;
gap: 2rem;
}
.sidebar {
position: sticky;
top: 1rem;
}If the sidebar behaves strangely, try:
.sidebar {
position: sticky;
top: 1rem;
align-self: start;
}That keeps the sidebar sized to its own content instead of stretching in the grid area.
This is not always required, but it is a useful thing to check when sticky sidebars feel wrong inside grid or flex layouts. If the alignment itself is the confusing part, read Why align-items: center Feels Broken.
The sticky element cannot be taller than its available space
Sticky works best when the sticky element is shorter than the visible scrolling area.
If the sticky element is taller than the viewport, sticking it to the top can make part of it unreachable or awkward to scroll through.
For example, a long table of contents might look fine on a large screen but fail on a smaller laptop:
.toc {
position: sticky;
top: 2rem;
}If the table of contents is too tall, you may need to constrain it deliberately:
.toc {
position: sticky;
top: 2rem;
max-height: calc(100vh - 4rem);
overflow: auto;
}Now the sidebar can stick, but its own content can scroll if needed.
This should be used carefully. Nested scroll areas can be annoying if they are too small or not obvious. But for long sticky sidebars, it can be the right tradeoff.
Sticky table headers have their own traps
Sticky table headers are a common use case:
th {
position: sticky;
top: 0;
}This can work well, especially inside a scrollable table wrapper.
But there are details to watch:
.table-wrapper {
overflow: auto;
max-height: 400px;
}
th {
position: sticky;
top: 0;
background: white;
}The background matters because the sticky header will sit on top of scrolling content. Without a background, text and borders may visually overlap.
You may also need a stacking order:
th {
position: sticky;
top: 0;
z-index: 1;
background: white;
}If the first column is sticky too, the layering gets more complicated because the top-left cell has to sit above both the sticky row and sticky column.
Sticky tables are possible, but they are not just one property. They combine positioning, overflow, backgrounds, and stacking.
Sticky is not fixed
Sometimes the real issue is expecting sticky to behave like fixed.
This:
.header {
position: sticky;
top: 0;
}Means the header participates in the normal layout and then sticks when it reaches the top.
This:
.header {
position: fixed;
top: 0;
}Means the header is removed from the normal layout and stays attached to the viewport.
Those are different tools.
Use sticky when the element belongs to a section and should stop sticking when that section ends.
Use fixed when the element should stay attached to the viewport regardless of the surrounding content.
A site header that stays visible across the entire page might be fixed or sticky depending on the layout.
A table heading, section label, article sidebar, or local navigation is usually a better fit for sticky.
Sticky can reveal stacking problems
A sticky element may need to appear above nearby content while it is stuck.
If it slides under other elements, you may need:
.sticky-header {
position: sticky;
top: 0;
z-index: 10;
background: white;
}The background is just as important as the z-index. Without it, content may scroll underneath and remain visible through transparent areas.
But if z-index does not work, you may be dealing with a stacking context problem.
A parent with properties like transform, opacity, filter, or certain positioning rules can create a new stacking context. In that case, increasing the sticky element's z-index may not put it above elements outside that context.
That is not specifically a sticky problem, but sticky elements often make it noticeable.
When a sticky element is behind something, check both its z-index and the stacking contexts around it.
A practical sticky sidebar pattern
For article pages, this is a solid starting point:
<div class="article-layout">
<aside class="toc">
...
</aside>
<article class="article">
...
</article>
</div>.article-layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 2rem;
}
.toc {
position: sticky;
top: 2rem;
align-self: start;
}
.article {
min-width: 0;
}There are a few useful details here.
align-self: start prevents the sidebar from stretching.
minmax(0, 1fr) and min-width: 0 help the article column handle real content without causing horizontal overflow. That minimum-size issue is the same family of bug covered in Why Flex Items Refuse to Shrink.
The sticky element is inside the larger layout region, not trapped inside a tiny parent.
And there is no unnecessary overflow: hidden on the layout wrapper.
That combination avoids many sticky bugs before they start.
A debugging checklist
When position: sticky does not work, check these in order:
- Does the element have
position: sticky? - Does it have an offset like
top,bottom,left, orright? - Which ancestor is the nearest scrolling ancestor?
- Do any parents have
overflow: hidden,overflow: auto, oroverflow: scroll? - Is the sticky element inside a parent that is too short?
- Is the sticky element taller than the available viewport space?
- Is it inside a grid or flex layout where
align-self: startwould help? - Is another element covering it because of
z-indexor stacking contexts? - Does it need a background while content scrolls underneath?
- Should this actually be
position: fixedinstead?
The most important habit is to inspect the ancestors.
Sticky bugs are often caused by a wrapper you forgot about.
The rule of thumb
A sticky element needs four things:
1. position: sticky
2. an offset, like top: 0
3. enough room inside its parent to move
4. no unexpected scrolling ancestor changing the contextFor a sticky sidebar, this is a good default:
.sidebar {
position: sticky;
top: 2rem;
align-self: start;
}Then inspect the parent chain for overflow rules.
position: sticky is not unreliable. It is just sensitive to the structure around it.
Once you understand that it sticks within a scrolling context and a parent boundary, most sticky bugs stop feeling random.
The element was not ignoring you. It was sticking exactly where the surrounding layout allowed it to stick.