CSS position:sticky for Sidebar Layouts: top, bottom, z-index and iOS Bugs
AI generated
CSS · Layout · Sidebar · Scroll
CSS position:sticky
Sidebar Layouts: top, bottom, z-index and iOS Bugs

position:sticky looks simple on paper, yet in practice it fails for a surprising number of reasons. This article walks through every single pitfall: overflow traps on the parent element, missing threshold properties, z-index inside stacking contexts, and why iOS Safari behaves differently.

14 min read position:sticky · top · bottom · overflow · z-index · iOS All modern browsers · iOS Safari

1. The Basic Principle of position:sticky

position:sticky is a hybrid positioning model: an element first behaves like position: relative, taking up its normal spot in the document flow. As soon as the user scrolls far enough that the element would cross its defined threshold (for example top: 20px from the visible viewport edge), it switches to behaving like position: fixed, but only within its scroll container. That combination makes it ideal for sidebars that should follow the user while scrolling, but only for as long as the parent content area lasts.

Understanding the "scroll container" is the key to using position:sticky correctly. A sticky element does not stick to the viewport, it sticks to its nearest scrolling ancestor. If there is no scrolling ancestor, the whole document scrolls and the element sticks to the viewport edge. However, as soon as an ancestor has overflow: auto, overflow: scroll or overflow: hidden, that ancestor becomes the scroll container, with all the consequences that has for sticky behavior.

2. Three Conditions That Must All Be Met

For position:sticky to work, exactly three conditions must be satisfied at the same time. First, the element must explicitly define a threshold: at least one of top, bottom, left or right must be set to a concrete value (not auto). Without that value the element always stays relatively positioned, even if position: sticky is set. Second, the element must sit inside a scrolling container that actually scrolls and is not clipped. Third, the parent element must not have overflow: hidden or overflow: auto set on itself, unless it is meant to be the scroll container.

There is another, often overlooked condition: the sticky element needs enough "room" inside its parent to actually stick. If the parent element (the sticky container) is exactly as tall as the sticky element itself, there is no distance left to scroll through, so the element sticks immediately and reaches the end of the parent right away, with no visible effect. That is not a bug, it is correct behavior: position:sticky only sticks within the height of its direct parent element.


/* Correct sticky sidebar layout, all three conditions met */

.page-layout {
  display: grid;
  grid-template-columns: 1fr 300px;
  gap: 2rem;
  align-items: start; /* IMPORTANT: prevents sidebar column from stretching */
}

.main-content {
  /* Main content can be any height */
}

.sidebar {
  position: sticky;
  top: 1.5rem; /* Threshold: stick when 1.5rem from top of viewport */
  /* No overflow on parent: document is the scroll container */
}

/* WRONG: this breaks sticky immediately */
.page-layout-broken {
  overflow: hidden; /* Kills sticky for all descendants */
}

/* WRONG: missing top value, sticky never activates */
.sidebar-broken {
  position: sticky;
  /* top is 'auto' by default, no threshold, no sticky behavior */
}

3. The Overflow Trap: the Most Common Cause of Sticky Failure

The overflow trap is by far the most common reason position:sticky stops working. As soon as any ancestor of the sticky element has overflow: hidden, overflow: auto or overflow: scroll set, that ancestor becomes the sticky element's scroll container. Since it does not scroll itself (for example because overflow: hidden clips content instead of making it scrollable), the sticky element sticks inside that clipped area. The result: the element looks like it should behave normally, but the expected sticking effect never happens.

What makes this especially tricky is that overflow: hidden is often set for unrelated layout reasons, for example to contain floated child elements (clearfix), to hide overflowing content, or to create a BFC (block formatting context). Anyone placing a sticky element inside a layout with a clearfix or a BFC container needs to make sure no overflow value changes the scroll container along the way. The fix for clearfix without overflow side effects is the display: flow-root pattern, or the ::after { content: ''; display: table; clear: both; } method, neither of which creates a BFC through overflow.


/* Diagnosing and fixing sticky overflow traps */

/* TRAP 1: Parent has overflow:hidden (e.g. old clearfix) */
.container-old-clearfix {
  overflow: hidden; /* Creates BFC for floats, but kills sticky */
}

/* FIX: Use display:flow-root instead */
.container-flow-root {
  display: flow-root; /* Creates BFC without touching overflow */
}

/* TRAP 2: Ancestor with overflow:auto for scrollable content */
.page-wrapper {
  overflow: auto; /* Becomes scroll container, sticky children stick here */
  height: 100vh;
}

/* If this is the desired scroll container, that's fine.
   But if sticky should respond to document scroll, remove overflow:auto */

/* TRAP 3: CSS transforms create new stacking context AND scroll container in some cases */
.animated-parent {
  transform: translateZ(0); /* GPU layer hack, can affect sticky in older browsers */
}

/* SOLUTION: Isolate transforms away from sticky containers */
.animated-child {
  transform: translateZ(0); /* Apply only to the element that needs it */
}

/* DEBUGGING SNIPPET: Find which ancestor is the scroll container */
/* In DevTools: Elements panel → Computed → Scroll Container indicator */

4. Top vs. Bottom Sticky: Difference and Use Cases

Most position:sticky implementations use top: the element sticks to the top edge of the scroll container as soon as it would cross that distance. The classic pattern is a sidebar that stays at the top while scrolling down and moves along with the user. The bottom counterpart is less well known but just as useful: an element with position: sticky; bottom: 0 sticks to the bottom edge of the scroll container. It starts at its normal position, and if the user scrolls up so far that it would disappear from view, it stays pinned to the bottom viewport edge instead.

An advanced pattern combines top and bottom for a sidebar that sticks both at the top and the bottom, depending on scroll direction and content height. This only works if the sticky element is taller than the available viewport area: when scrolling down, the bottom edge sticks first (bottom), and when scrolling up, the top edge sticks (top). If the element is smaller than the viewport, only one direction ever applies. This pattern is useful for long desktop navigation sidebars.

5. Sticky Inside Scroll Containers

When the scroll container is not the document itself but a div element with overflow: auto and a fixed height, the same rules apply to position:sticky, just relative to that container instead. The sticky element sticks to the edge of the scrolling container, not the viewport. That is intentional and enables tables with sticky headers or scrollable lists with sticky group headers.

A common mistake in this context: setting position: sticky; top: 0 on a table cell element (th) while the scroll container is not the table itself but a wrapping div. In that case the wrapping div needs overflow: auto and a fixed height for the sticky table headers to work correctly. Alternatively, overflow: auto can be set directly on the table element, but that changes the table's layout behavior and requires explicit column widths.


/* Sticky table header in a scroll container */
.table-wrapper {
  overflow: auto;
  max-height: 500px;
  border-radius: 12px;
  border: 1px solid #e2e8f0;
}

table {
  width: 100%;
  border-collapse: collapse;
}

thead th {
  position: sticky;
  top: 0; /* Sticks to top of .table-wrapper scroll container */
  background: #1e293b;
  color: white;
  padding: 0.75rem 1rem;
  text-align: left;
  z-index: 1; /* Must be above tbody cells */
  /* border-bottom visible below sticky header */
  box-shadow: 0 1px 0 #334155;
}

/* Sticky group header within a scrollable list */
.grouped-list {
  overflow: auto;
  max-height: 400px;
}

.group-header {
  position: sticky;
  top: 0;
  background: #f8fafc;
  padding: 0.5rem 1rem;
  font-weight: 700;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #64748b;
  border-bottom: 1px solid #e2e8f0;
  z-index: 1;
}

6. z-index and Stacking Contexts With Sticky Elements

position:sticky creates a new stacking context, just like position: fixed or position: absolute. That means a sticky element's z-index competes with other stacking contexts at the same level, not with every element globally. This leads to a classic problem: a sticky navigation bar overlaps a dropdown or popover that lives inside the same scroll container, because both share the same stacking context.

The correct fix is to define z-index values deliberately as part of a consistent system. A sticky header might get z-index: 10, dropdowns in the content area z-index: 20 (so they appear above the sticky nav when opened), and modal overlays z-index: 50. The antipattern is z-index: 9999, which does not solve structural stacking problems, it just adds new ones. Using CSS custom properties for z-index values (--z-sticky: 10; --z-dropdown: 20;) keeps the system maintainable.

7. iOS Safari: Known Sticky Bugs and Workarounds

iOS Safari has historically had trouble with position:sticky in ways that differ from desktop Safari. The best known bug affected iOS Safari up to version 13: sticky simply did not work without the vendor prefix -webkit-sticky. This has been fixed since iOS 14, but older devices are still in use. The workaround, position: -webkit-sticky; position: sticky;, is harmless and should still be included.

A newer iOS Safari issue involves the interaction between position:sticky and the address bar behavior. iOS Safari dynamically adjusts the viewport height as the address bar shows or hides. That can cause sticky elements to briefly "jump" when the address bar changes height. The workaround is to use min-height: 100dvh (dynamic viewport height) instead of 100vh for the main scroll container, so the viewport reference point stays stable. Another iOS specific issue: sticky inside a CSS grid or flexbox column can fail under certain circumstances if align-self or align-items is left at the default stretch; the fix is align-self: start on the sticky element.

Problem Cause Solution Browser
sticky does not work overflow:hidden on an ancestor display:flow-root instead of overflow:hidden All
sticky has no effect No top/bottom set Set top: 0 or a concrete value All
sticky ends too early Parent element too small align-items: start on the grid/flex container All
sticky hidden behind other elements z-index conflict Consistent z-index system with CSS custom properties All
sticky does not work on iOS iOS Safari < 14 Add the -webkit-sticky prefix iOS Safari

9. Debugging Sticky: Checklist and Browser DevTools

When position:sticky is not behaving as expected, a structured checklist helps more than blind experimentation. Step 1: is an explicit top or bottom value set? Step 2: does an ancestor of the sticky element have overflow: hidden, auto or scroll? Step 3: is the parent element of the sticky element (the sticky container) tall enough, clearly taller than the sticky element itself? Step 4: is the sticky element inside a grid or flex column? If so, set align-self: start. Step 5: is it iOS? Is the -webkit-sticky prefix present?

Since version 94, Chrome DevTools shows a hint in the Computed panel whenever a sticky element is not sticking, and names the specific reason. The selector is flagged with a small "sticky" badge, and the explanation shows whether an overflow ancestor, a missing threshold, or a parent that is too small is causing the problem. This information is more valuable than any trial and error debugging session and should be the first stop when troubleshooting sticky issues. Firefox DevTools offers a similar layout panel.

Mironsoft

CSS layout, Hyva themes and frontend architecture

Need to fix sticky layouts and complex CSS structures?

We analyze layout problems in Magento and Hyva projects and resolve sticky conflicts, z-index chaos and overflow traps systematically, with clean, maintainable CSS and no hacks.

Layout Audit

Systematic analysis of z-index, overflow and sticky conflicts in existing CSS

Sidebar Development

Sticky navigation, product sidebars and filter bars for Hyva themes

Cross-Browser QA

iOS Safari, Chrome, Firefox and Edge: tested sticky solutions with no browser bugs

10. Summary

CSS position:sticky is one of the most useful layout properties for sidebar designs, but it comes with clear requirements that must all be met at once. The most common mistake is an overflow: hidden or overflow: auto on an ancestor that switches the scroll container. The three core conditions, an explicit threshold (top/bottom), no overlapping overflow on an ancestor, and enough height in the parent element, should always be checked first before looking for more exotic causes.

For grid and flexbox layouts, align-self: start on the sticky element is mandatory, otherwise the parent gets stretched to the same height as the content area and no scroll room remains. For iOS Safari, position: -webkit-sticky belongs as the first value in every sticky declaration. The Chrome DevTools sticky badge is the fastest debugging tool available. And a consistent z-index system built on CSS custom properties keeps sticky elements from disappearing behind other content.

position:sticky: the essentials at a glance

Required conditions

Set top or bottom explicitly. No overflow:hidden on an ancestor. The parent element must be clearly taller than the sticky element.

Grid/flex layouts

Set align-self: start on the sticky element, preventing the grid/flex item from being stretched to column height.

iOS Safari

Add the -webkit-sticky prefix. Use min-height: 100dvh for a stable viewport height. Set align-self: start in flex/grid columns.

z-index system

Use CSS custom properties for z-index values. Layer sticky nav, dropdowns and modals clearly. Never use z-index: 9999.

11. FAQ: CSS position:sticky for Sidebar Layouts

1Why doesn't my position:sticky work?
Most common causes: no top/bottom set, overflow:hidden/auto on an ancestor, parent element not tall enough, missing align-self:start in grid/flex.
2Why does sticky stop before the page ends?
The sticky element only sticks within its parent element (the sticky container). This is correct behavior, the parent element limits the sticking area.
3Clearfix without breaking sticky?
display:flow-root instead of overflow:hidden. Creates the same BFC for floats without changing overflow, sticky stays functional.
4Sticky sidebar in CSS grid?
Grid container: align-items: start. Sidebar element: position: sticky; top: [value]. Without align-items:start, grid stretches the column to the other column's height, leaving no room.
5sticky vs. fixed?
fixed always sticks to the viewport and takes up no space in the flow. sticky only sticks within the scroll container, stays in the flow and ends at the parent element's edge.
6z-index conflict with a dropdown?
Consistent system: sticky nav z-index:10, dropdowns z-index:20, modals z-index:50. CSS custom properties for maintainability. Never z-index:9999.
7Sticky not working on iOS?
Add the -webkit-sticky prefix (harmless on newer versions). For viewport jumps: min-height: 100dvh instead of 100vh.
8Combining top and bottom sticky?
Yes, for elements larger than the viewport. Scrolling down: bottom applies. Scrolling up: top applies. Ideal for long desktop navigation trees.
9Debugging sticky in Chrome DevTools?
Chrome 94+: 'sticky' badge in the Elements panel. The Computed panel shows the specific reason for failure: overflow ancestor, missing threshold, or parent too small.
10Animating a sticky header on scroll?
Intersection Observer API: observe a sentinel element at the top of the page. When it is not visible: set a class, trigger a CSS transition for box-shadow or background.