Why position:sticky Stops Working (and How to Fix It)

Quick answer

💡position:sticky fails most often because a parent or ancestor has overflow:hidden, overflow:auto, or overflow:scroll — this breaks the sticky scroll context. Check that you have set a top, bottom, left, or right offset, and that the sticky element's parent container is taller than the element itself.

Error symptoms

  • Element scrolls away instead of sticking to the viewport edge
  • Sticky works in a simple demo but not in the real app
  • Element sticks briefly then disappears mid-scroll
  • Header sticks but table headers inside a scrollable table do not
  • Element sticks on desktop but not on mobile Safari
  • position:sticky seems to behave the same as position:relative

Common causes

  • A parent or ancestor has overflow:hidden, overflow:auto, or overflow:scroll
  • No threshold offset set — top, bottom, left, or right is missing
  • The sticky element's parent container is too short — sticky stops at the parent's bottom edge
  • The container height equals or is less than the element height, leaving no room to stick
  • Safari older than 13 requires -webkit-sticky as well as sticky
  • Sticky inside a flex container that is itself not the scroll container

When it happens

  • After adding overflow:hidden to a layout wrapper to clear floats
  • When pasting sticky into an existing component without checking the parent chain
  • After switching from a fixed layout to a flex or grid layout
  • When building sticky table headers inside a div with overflow:auto

Examples and fixes

A single overflow:hidden on a parent silently cancels sticky for all children.

Parent overflow breaks sticky

❌ Wrong

<div class="wrapper">
  <nav class="sticky-nav">Menu</nav>
  <main>Long content...</main>
</div>

.wrapper { overflow: hidden; }
.sticky-nav {
  position: sticky;
  top: 0;
}

✅ Fixed

<div class="wrapper">
  <nav class="sticky-nav">Menu</nav>
  <main>Long content...</main>
</div>

.wrapper { overflow: visible; }
.sticky-nav {
  position: sticky;
  top: 0;
}

When a parent has overflow:hidden, the browser creates a new scroll container scoped to that element. Because the sticky element scrolls within that container — and the container clips all overflow — there is no visible scrolling for sticky to respond to. Setting overflow:visible removes this restriction. If you need overflow:hidden for clipping purposes, wrap only the clipped content in a separate element and keep the sticky element outside of it.

position:sticky without a top/bottom/left/right offset behaves like position:relative.

Missing threshold offset

❌ Wrong

<header class="sticky-header">Logo</header>

.sticky-header {
  position: sticky;
  background: white;
  z-index: 10;
}

✅ Fixed

<header class="sticky-header">Logo</header>

.sticky-header {
  position: sticky;
  top: 0;
  background: white;
  z-index: 10;
}

The CSS specification requires a threshold offset to determine where in the scroll flow the element transitions from relative to fixed positioning. Without top:0 (or the appropriate directional property), the browser has no defined sticking point and treats the element as if it were relatively positioned. Always set the offset to match the direction you want the element to stick — top:0 for a sticky header, bottom:0 for a footer that stays visible at the bottom of a scrollable region.

Why sticky fails in real projects

The most common culprit is an ancestor element with overflow set to anything other than visible. When a browser encounters overflow:hidden, overflow:auto, or overflow:scroll on a parent, it creates an independent scroll container. The sticky element then tries to stick within that container — but because the container clips its overflow, the sticky behavior is invisible or non-functional. This is the number one reason sticky works in a pen or demo but breaks when you paste the same CSS into a real app with existing layout wrappers.

The second most frequent cause is a missing threshold offset. position:sticky is effectively a hybrid of relative and fixed positioning. It behaves like relative until the user scrolls past a threshold defined by top, bottom, left, or right. If none of those properties is set, there is no threshold, and the element stays relative forever. Developers often set position:sticky and move on, forgetting that top:0 is required to make the transition happen.

Container height is a subtler issue. Sticky positioning stops when the sticky element reaches the bottom edge of its parent container. If the container is exactly as tall as the sticky element — or shorter — there is no room to scroll, and the element never transitions to its stuck state. This often appears when a flex or grid container only wraps the sticky element and nothing else.

Finally, there are browser-specific considerations. Safari required the vendor prefix -webkit-sticky before version 13, released in 2019. If you support older Safari versions, you need both position:-webkit-sticky and position:sticky. On iOS, body-level scrolling behaves differently from overflow:scroll containers, and sticky elements inside transformed ancestors can also fail unexpectedly.

How to diagnose with DevTools

Open Chrome or Firefox DevTools and select the sticky element. In the Computed panel, look for the position value. If it shows relative instead of sticky, the declaration has been overridden — check the Rules panel for a strikethrough. If position:sticky is present and not overridden, the element is declared correctly but failing for a different reason.

Next, use the Elements panel to walk up the DOM tree from the sticky element. For each ancestor, check the Computed tab for overflow. Look for any value that is not visible — even overflow-x:hidden on a parent that you expected to only affect horizontal scrolling can break vertical sticky behavior, because browsers require the scroll container to permit overflow in all directions.

To confirm the scroll container issue quickly, add overflow:visible to the suspected ancestor in DevTools (no page reload needed) and then scroll the page. If the element suddenly sticks, you found your culprit. From there, decide whether to remove overflow:hidden or restructure the layout so the sticky element sits outside the clipping container.

For mobile debugging, use Safari's Web Inspector connected via USB to test on a real iOS device, or use Chrome's remote debugging for Android. Sticky inside overflow:auto containers behaves differently on mobile Safari and should be tested on actual hardware when the layout is complex. Chrome DevTools' device emulation mode does not fully replicate iOS scroll behavior.

Chrome DevTools has a sticky positioning badge that appears next to elements with position:sticky in the Elements panel — a small icon that indicates whether the sticky element is currently in its stuck state. When you scroll the page with DevTools open and the element panel visible, this indicator updates in real time, confirming whether the element is actually entering its stuck state or is failing to do so. This visual cue is faster than reading Computed values and helps narrow down whether the issue is a CSS override, an overflow problem, or insufficient container height.

Concrete fixes for each root cause

If the cause is overflow:hidden on a parent, you have three options. First, remove overflow:hidden if it was only added to clear floats — modern layouts with flexbox or grid do not need float clearing. Second, if you need overflow:hidden for visual clipping, move the sticky element outside the clipping container so it is a sibling, not a child. Third, use a CSS Isolation wrapper: place the sticky element in its own container that has no overflow restrictions, and wrap the clipped content in a separate sibling.

If the cause is a missing offset, add top:0 for a sticky header, bottom:0 for a footer, or the appropriate value for your layout. Remember that top and bottom can take non-zero values too — top:60px is useful for a sticky sidebar that needs to clear a fixed header above it.

If the sticky element stops too early because the parent container is too short, you need to restructure the markup. The sticky element and the content it should scroll past must share the same parent. A sticky sidebar works correctly only when it and the article content are siblings inside a shared container. If your sidebar is nested inside a short wrapper, it will stop sticking as soon as that wrapper ends.

For Safari compatibility, always include both position:-webkit-sticky and position:sticky in that order. Place -webkit-sticky first so that browsers ignoring unknown properties will still apply the standard property.

You can use @supports (position: sticky) to detect sticky support and apply a JavaScript fallback for browsers that do not support it. Inside an @supports block, declare the sticky styles; outside it, apply a fallback such as position:fixed or a JavaScript-driven scroll listener. This progressive enhancement pattern ensures that layouts remain functional on browsers without sticky support while delivering the optimal CSS-only experience where it is available.

Edge cases that trip up developers

Sticky headers inside a sticky table are one of the most requested combinations and they work — but only if the table's scroll container has enough height and does not have overflow:hidden. Using position:sticky on a th element with top:0 makes table headers stick while the table body scrolls, but the table itself must be inside a container with overflow:auto and a defined max-height, not overflow:hidden.

Transforms on ancestors create a new stacking context and can affect sticky behavior. If an ancestor has transform:translate(0,0) applied — a common performance hack — it may create a compositing layer that interferes with how the browser handles the scroll container. Test sticky behavior before and after adding transforms to parent elements in your layout.

Sticky inside a flex container requires care. If the flex container is the scroll container (it has overflow:auto and a fixed height), then position:sticky on a flex child works normally within that container. However, if the flex container grows to fit its children rather than scrolling, there is no scroll context and sticky will not function. The key question is always: which element is the scroll container for the sticky element?

On iOS Safari specifically, elements with position:sticky inside containers that have -webkit-overflow-scrolling:touch can behave inconsistently. This is a known longstanding bug. The most reliable workaround is to remove -webkit-overflow-scrolling and use the standard overflow:auto, which now performs momentum scrolling natively in modern iOS versions without the legacy property.

When multiple sticky elements overlap — for example, a sticky navigation bar and a sticky section header that both use top:0 — they will stack on top of each other rather than push each other down. To make a lower sticky element stick below an already-stuck higher one, set the lower element's top offset to match the height of the element above it: top:60px if the nav bar is 60px tall. Using CSS custom properties for these offsets — --nav-height: 60px — makes the relationship explicit and easy to maintain when the header height changes.

Common mistakes to avoid

The most repeated mistake is adding overflow:hidden to a layout wrapper to prevent content from overflowing horizontally, without realizing it will break all sticky elements inside. If you need to prevent horizontal overflow, consider using overflow-x:clip instead of overflow:hidden — clip does not create a scroll container and preserves sticky behavior in many cases.

A second mistake is assuming that z-index alone will fix sticky not appearing above sibling content. z-index only works on positioned elements. A sticky element is positioned, so z-index applies — but if you have not set z-index at all, the element may appear behind siblings that were painted later. Always add z-index:10 (or higher) to sticky headers and navigation bars.

Developers also sometimes confuse position:sticky with position:fixed. Fixed positioning always anchors the element relative to the viewport, regardless of scroll context. Sticky is relative to the nearest scroll container and respects the parent's bounding box. If you want a global header that always covers the viewport regardless of DOM structure, fixed is the correct choice.

Finally, forgetting to add a background color to the sticky element is a visual mistake that makes the page look broken. When the element transitions from scrolling to stuck, it overlaps scrolled content. Without a background, that content shows through. Always set a background that matches the page or header design.

Another often-overlooked mistake is not accounting for scroll-margin-top when page content uses sticky headers. When a user navigates to an anchor link on the page, the browser scrolls the target element to the top of the viewport — directly behind a sticky header. The target content is technically at the top but hidden under the header. Setting scroll-margin-top on anchor targets (or using the global scroll-padding-top on the html element) offsets the scroll destination by the height of the sticky header, so linked content is always fully visible.

Best practices for reliable sticky layouts

Always audit the overflow property on every ancestor of a sticky element before shipping. Use DevTools to walk up the tree, or write a quick JavaScript snippet during development that logs all ancestors with non-visible overflow. This takes two minutes and prevents the most common source of sticky bugs.

Structure your markup intentionally for sticky. The sticky element and the content it should scroll past must be siblings inside the same parent. If you are building a sticky sidebar, your HTML structure should be a wrapper with two children: the sidebar and the main content area. The wrapper must have enough height (set by the content, not a fixed pixel value) for sticky to work across all screen sizes.

Set explicit z-index values on all sticky elements and document them. Use CSS variables to define a stacking scale — for example, --z-sticky:100; --z-modal:200 — so that all sticky headers, sidebars, and overlays have predictable layering. This avoids sticky headers appearing behind dropdowns or modals that were added later.

Test on real devices, not just browser emulation. iOS Safari, Samsung Internet, and Firefox on Android all handle sticky inside complex flex or grid layouts slightly differently. Allocate time in your QA pass for real-device scroll testing, especially for layouts with multiple nested sticky elements.

Consider using IntersectionObserver as a progressive enhancement or fallback for browsers where sticky behaves unexpectedly. An IntersectionObserver watching a sentinel element just above the sticky container can detect when the page has scrolled past the threshold and apply a .is-stuck class via JavaScript. This class can drive visual changes such as box-shadow or border appearance that signal the stuck state to users — effects that cannot be achieved with pure CSS sticky alone, since there is no :stuck pseudo-class in current CSS.

When building sticky table headers specifically, apply position:sticky and top:0 directly to each th element inside thead, not to the thead row itself. Browser support for sticky on tr and thead is inconsistent. Applying it per th ensures predictable behavior across Chrome, Firefox, and Safari. Pair each th with a background-color declaration so the table body rows do not show through when scrolling past the header.

Quick fix checklist

  • Check all ancestors for overflow:hidden, overflow:auto, or overflow:scroll
  • Confirm top, bottom, left, or right offset is set on the sticky element
  • Verify the parent container is taller than the sticky element
  • Add a background color so the sticky element covers scrolled content
  • Add z-index so the element appears above siblings when stuck
  • Test on real iOS device — Safari scroll behavior differs from emulation
  • Replace overflow:hidden with overflow-x:clip if only horizontal clipping is needed
  • Ensure the sticky element and the content it scrolls past share the same parent

Related guides

Frequently asked questions

Why does position:sticky work in CodePen but not in my app?

CodePen has a minimal DOM with no overflow restrictions. Your app almost certainly has a layout wrapper with overflow:hidden or overflow:auto that creates a scroll container, preventing sticky from working as expected. Walk up the DOM tree in DevTools and look for any ancestor with a non-visible overflow value.

Can I use position:sticky inside a flex or grid container?

Yes, but the flex or grid container must be the scroll container, meaning it needs overflow:auto or overflow:scroll and a defined height. If the container grows to fit its content without scrolling, there is no scroll context and sticky will not activate.

Does position:sticky work on table headers?

Yes. Apply position:sticky and top:0 directly to the th or thead element. The table must be inside a scrollable container with a defined max-height and overflow:auto. This works in all modern browsers including Safari.

Why does my sticky element disappear partway through the page?

A sticky element stops at the bottom edge of its parent container. If the container ends before the page does, the sticky element scrolls away when the container ends. Restructure your markup so the sticky element and the content it should cover share the same tall parent.

Do I need -webkit-sticky for Safari?

Only if you support Safari older than version 13 (released 2019). Modern Safari supports the standard position:sticky without a prefix. To be safe in legacy projects, include both: position:-webkit-sticky; position:sticky; in that order.

Does overflow-x:hidden also break sticky?

Yes. overflow-x:hidden on a parent creates a scroll container in the same way that overflow:hidden does, and this breaks sticky for all descendants. Use overflow-x:clip instead — it visually clips content without creating a scroll container.

How do I make a sticky sidebar that stops before the footer?

Place the sidebar and main content as siblings inside a shared wrapper. Give the sidebar position:sticky; top:20px; and let the wrapper's natural height (determined by the taller of the two children) define the scroll range. The sidebar will unstick automatically when its parent container ends, before the footer begins.

Can position:sticky and position:fixed be used together on the page?

Yes, but on different elements. Use fixed for global UI like a top navigation bar that must always cover the full viewport. Use sticky for elements that should stick within a specific scroll container, like a sidebar or a section heading that sticks only while that section is in view.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.