You add position: sticky to a header, sidebar or table heading and it scrolls away like a normal element. No error, no warning, it just doesn’t stick. When position sticky is not working, the cause is nearly always one of a handful of layout rules, and this guide covers each one with the exact CSS to fix it plus a DevTools routine to find the culprit fast.
Quick fix
Check these three things in order; they cover the large majority of cases:
- Add an offset. Sticky does nothing without
top,bottom,leftorright. - Find ancestors with
overflowset. Any ancestor withoverflow: hidden,autoorscrollbecomes the box the element sticks inside. Replacehiddenwithclip. - Stop flex or grid stretching. In a flex row or grid, add
align-self: flex-start(orstart) to the sticky item.
.sidebar {
position: sticky;
top: 1rem;
align-self: flex-start; /* if the parent is display: flex */
}
.page-wrapper {
overflow-x: clip; /* instead of overflow-x: hidden */
}
How sticky actually works
A sticky element behaves like relative until its nearest scroll container scrolls it past the offset you set; then it stays put, but only while its parent (its containing block) is still on screen. Two rules follow from that, and almost every bug breaks one of them: the element must stick relative to the container that actually scrolls, and its parent must be taller than it so there is room to move.
Causes, most common first
1. No top or bottom offset
Without an inset value the browser has no threshold to stick at, so the element stays in normal flow. Use top for headers and sidebars, bottom for footers or action bars that should stick to the bottom edge, and left for the first column of a horizontally scrolling table.
2. An ancestor with overflow hidden, auto or scroll
Setting overflow to anything other than visible or clip turns that element into a scroll container, even if it never scrolls. The sticky element then sticks inside that box. If that box grows with its content and the page scrolls instead, the sticky element has nothing to react to and looks broken.
A common trap is overflow-x: hidden added to stop horizontal scrollbars. When one axis is hidden and the other is visible, the browser computes the visible one as auto, so the element becomes a scroll container on both axes.
On body there is a twist. If only body has overflow-x: hidden, the value is propagated to the viewport and sticky usually keeps working. The trouble starts with the popular reset html, body { overflow-x: hidden; }: the html value goes to the viewport, body keeps its own and becomes a scroll container that never scrolls, and every sticky element on the page stops sticking.
The fix is overflow: clip, which clips content without creating a scroll container:
/* Before: breaks sticky */
html,
body {
overflow-x: hidden;
}
/* After: clips horizontal overflow, sticky still works */
body {
overflow-x: clip;
}
overflow: clip is supported in all current major browsers. If you need older Safari, fix the element that actually overflows (often a full-bleed section with 100vw width or a negative margin) instead of hiding overflow on a wrapper.
3. The parent is the same height as the sticky element
A sticky element can only move within its parent. If you wrap a header in a <div> that contains nothing else, the wrapper is exactly as tall as the header, so there is zero distance to stick across. Move position: sticky to the wrapper, or restructure so the sticky element’s parent is a tall container such as the page layout or the article.
/* Header is the only child of .header-wrap: won't stick */
.header-wrap .site-header { position: sticky; top: 0; }
/* Make the wrapper itself sticky instead */
.header-wrap { position: sticky; top: 0; z-index: 10; }
4. Flex and grid stretching (align-self: stretch)
Flex items and grid items stretch to fill the cross axis by default. A sidebar in display: flex becomes as tall as the main column, which is the same problem as cause 3: no room to move. Stop the stretch on the sticky item:
.layout {
display: flex;
gap: 2rem;
align-items: flex-start; /* or align-self on the sidebar only */
}
.layout-grid {
display: grid;
grid-template-columns: 1fr 300px;
}
.layout-grid .sidebar {
position: sticky;
top: 1rem;
align-self: start;
}
5. Tables and display: table quirks
For sticky table headers, put position: sticky on the <th> cells; that has the widest support. With border-collapse: collapse, cell borders belong to the table rather than the cell and do not travel with the sticky header, so the header looks borderless while scrolling. Use border-collapse: separate with border-spacing: 0, or draw the line with a box-shadow. Also give sticky cells a background, or rows show through them. If the table sits inside a wrapper with overflow-x: auto for mobile, the header sticks to that wrapper, not the page, which is cause 2 again.
6. Transforms, and a common myth
A transform, filter or will-change: transform on an ancestor changes the containing block for position: fixed elements, which is why fixed headers inside animated wrappers break. It does not by itself stop position: sticky, because sticky follows the nearest scroll container, not transforms. If sticky fails inside a transformed wrapper, check that wrapper for overflow first; animation libraries and slider wrappers often set it. A transform on the sticky element itself is fine, but it shifts where it appears visually, which can look like the offset is wrong.
7. Stacking: it sticks but you can’t see it
Sometimes sticky works and the element is simply painted underneath later content. Give it a background and a z-index, since sticky creates a positioned element that later siblings can cover.
DevTools debugging routine
- Select the sticky element in the Elements panel and confirm in Computed that
positionisstickyand an inset such astopis notauto. - Check its parent’s height in the box model view. If it matches the sticky element, see causes 3 and 4.
- Walk up the ancestors looking for
overflow. Recent Chromium DevTools also mark scroll containers with ascrollbadge in the Elements tree. Or run this in the Console with the element selected:
// $0 is the element currently selected in the Elements panel
(function findScrollAncestors(el) {
let node = el.parentElement;
while (node) {
const s = getComputedStyle(node);
if (![s.overflowX, s.overflowY].every((v) => v === 'visible' || v === 'clip')) {
console.log(node, 'overflow-x:', s.overflowX, 'overflow-y:', s.overflowY);
}
node = node.parentElement;
}
})($0);
- Toggle off each reported
overflowrule in the Styles pane. If sticky starts working, swap that rule forclipor remove it. - If nothing is reported and the parent is tall enough, check
z-indexand background to rule out a painting issue.
Prevention
- Never put
overflow-x: hiddenonhtmlandbodytogether; fix the overflowing element or useclip. - Use
overflow: clipfor decorative clipping by default and reservehiddenorautofor real scroll areas. - Put sticky on the element whose parent is the full layout column, not on a tightly wrapped child.
- In flex and grid layouts, set
align-self: starton sticky items as a habit.
Key takeaways
- Sticky needs an offset like
top: 0. - Any ancestor with
overflowhidden, auto or scroll becomes the sticky element’s scroll container. html, body { overflow-x: hidden; }silently breaks every sticky element on the page;overflow-x: clipdoesn’t.- The parent must be taller than the sticky element; flex and grid stretching removes that room.
- Transforms break
position: fixed, not sticky. Look for overflow instead.
Frequently asked questions
Why is position sticky not working in my flex container?
Flex items stretch to the container’s height by default, so the sticky item has no room to move. Add align-self: flex-start to the sticky item or align-items: flex-start to the container.
Does overflow hidden break position sticky?
Yes. overflow: hidden, auto or scroll on any ancestor makes it the scroll container the element sticks to. Use overflow: clip instead, which clips without creating a scroll container.
Why does my sticky header stop halfway down the page?
A sticky element only sticks while its parent is on screen. When the parent’s bottom edge scrolls past, the element leaves with it, so move position: sticky to a higher-level container.
Do I still need -webkit-sticky?
Only if you support Safari versions older than 13. Every current browser supports unprefixed position: sticky.
If a layout still refuses to cooperate, contact me and I’ll help you fix it. Sticky elements that load late or change size can also cause layout shift, so see my guide to fixing Cumulative Layout Shift.