CSS position: sticky Guide
Interactive demo of sticky positioning — adjust offsets, explore common patterns, and troubleshoot why sticky isn't working
How sticky positioning works
Adjust sticky offset
Drag the slider to change the sticky threshold. Scroll the demo box below to see the effect.
Scroll container — scroll down to see the sticky element stick at top: 0px
Scroll Container
A sticky element finds its nearest ancestor that has overflow set to anything other than visible. That ancestor becomes the scroll container. If no such ancestor exists, the viewport is the scroll container. The sticky element only works within the bounds of this container — once the container scrolls out of view, the sticky element goes with it.
Threshold (Sticky Offset)
The threshold is defined by the offset property (top, bottom, left, or right). When the element's edge reaches this distance from the scroll container edge, it 'sticks' — switching from relative positioning to fixed-like behavior. The element behaves like position: relative until the threshold is reached, then like position: fixed until the containing block exits the viewport. At least one offset value must be specified for sticky to work.
Containing Block
A sticky element's stickiness is bounded by its containing block (its nearest block-level ancestor). The element will never move outside its containing block, even if it would otherwise stay sticky relative to the scroll container. When the containing block scrolls out of view, the sticky element scrolls with it. This is what creates the 'stacking sections' accordion effect when multiple sections each have sticky headings.
Generated CSS
Pattern: Sticky Header.site-header {
position: sticky;
top: 0px;
z-index: 100;
}You added position: sticky; top: 0 to your sidebar and… nothing happened. It just scrolls normally. You check the CSS — it looks right. You add z-index. Nothing. You try position: -webkit-sticky. Still nothing. Turns out there’s an overflow: hidden on a grandparent <div> three levels up that silently breaks sticky positioning. Or your grid container has align-items: stretch (the default), so the sidebar fills the full row height and has no room to “stick.” Sticky is the most deceptively simple CSS property — it works perfectly until it doesn’t, and the failure mode is always silent.
Why This Guide (Not Just “position: sticky top: 0”)
Most sticky tutorials show the two-line solution and stop. This guide covers the five most common reasons sticky fails (ancestor overflow, missing height, missing offset, no z-index, containing block too short) with interactive demos and fixes. Plus patterns for sticky headers, sidebars, table headers, and stacking sticky elements. Everything runs in your browser; no data is sent anywhere.
CSS position: sticky Guide
position: sticky is a hybrid positioning value — elements with sticky positioning behave like position: relative until they hit a defined scroll threshold, then switch to position: fixed-like behavior within their scroll container. This guide covers every aspect of sticky positioning with interactive demos, common patterns, and troubleshooting tips.
How CSS position: sticky Works
A sticky element switches between two modes as you scroll:
- Before the threshold: the element flows in the normal document flow (like
position: relative) - At the threshold: the element “sticks” to the defined offset and stays there while the scroll continues
- After the containing block exits: the element scrolls away with its containing block
.sticky-header {
position: sticky;
top: 0; /* sticks when it would scroll above the top of the viewport */
}
At least one offset property must be set (top, bottom, left, or right) for sticky to work. Without an offset, the element behaves like position: relative.
The Three Key Concepts
1. Scroll Container
The scroll container is the nearest ancestor with overflow set to anything other than visible. If no such ancestor exists, the viewport is the scroll container. The sticky element only works within this container — when the container scrolls out of view, the sticky element goes with it.
2. Threshold (Sticky Offset)
The threshold is the point defined by the offset property where the element switches from flowing to sticking. top: 0 means the element sticks when its top edge would otherwise scroll past the top of the viewport. bottom: 16px means it sticks when its bottom edge would otherwise scroll past 16px above the viewport bottom.
3. Containing Block
The containing block (the sticky element’s nearest block-level ancestor) limits the sticky range. The element will never move outside its containing block — even if it would otherwise stay sticky relative to the scroll container. This is how section headings “hand off” to one another as you scroll through a document.
Common sticky Offset Values
| Value | Effect |
|---|---|
top: 0 | Sticks flush to the top of the viewport |
top: 64px | Sticks 64px below the top (useful when there’s a fixed nav bar) |
bottom: 0 | Sticks flush to the bottom of the viewport |
left: 0 | Sticks to the left edge (horizontal scroll) |
right: 0 | Sticks to the right edge (horizontal scroll) |
Common Pattern: Sticky Header
A sticky header is the most common use of position: sticky. The header scrolls with the page until it reaches the top, then sticks in place.
.site-header {
position: sticky;
top: 0;
z-index: 100;
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
Key requirements:
- The header must be a direct child of the scrolling container (usually
<body>) background-colormust be set to prevent content from bleeding throughz-indexshould be set to ensure the header renders above scrolling content
Common Pattern: Sticky Sidebar
A sticky sidebar remains visible while the main content scrolls past it.
.layout {
display: grid;
grid-template-columns: 250px 1fr;
gap: 24px;
align-items: start; /* critical */
}
.sidebar {
position: sticky;
top: 24px;
max-height: calc(100vh - 48px);
overflow-y: auto;
}
Critical: align-items: start on the grid container is required. Without it, the sidebar stretches to the full grid row height and has no room to scroll within its containing block.
Common Pattern: Sticky Table Headers
.table-container {
max-height: 400px;
overflow-y: auto;
}
thead th {
position: sticky;
top: 0;
background-color: #f9fafb;
z-index: 10;
border-bottom: 2px solid #e5e7eb;
}
Note: With border-collapse: collapse, table borders can disappear when headers stick. Apply border-bottom directly to <th> elements instead.
Common Pattern: Stacking Sticky Elements
When multiple sections each have a sticky heading, they naturally “push each other out” as the user scrolls. Each heading sticks at the top of the viewport until its containing <section> scrolls away.
.section-header {
position: sticky;
top: 0;
background-color: #ffffff;
z-index: 10;
padding: 8px 16px;
}
This works because each heading’s containing block is its own <section>. The heading sticks until the section exits the viewport, at which point the next section’s heading takes over.
Troubleshooting: Why Is My Sticky Not Working?
Problem 1: Ancestor has overflow: hidden
overflow: hidden on any ancestor between the sticky element and the viewport will break sticky positioning. The sticky element becomes contained within that overflow-hidden box instead of the viewport.
Fix: Remove overflow: hidden from ancestors. Use clip-path or border-radius for visual clipping that doesn’t create a scroll container.
Problem 2: No defined height on scroll container
If sticky is scoped to a container (not the viewport), that container needs a height or max-height to actually scroll. Without one, the container expands to fit all content and sticky has nothing to stick to.
Fix: Add height or max-height to the scroll container.
Problem 3: Missing offset value
position: sticky without any offset property (top, bottom, left, or right) is equivalent to position: relative. The element must have at least one offset declared.
Fix: Add top: 0 (or whichever offset applies to your scroll direction).
Problem 4: No z-index (content scrolls over sticky element)
Without z-index, content may render on top of the sticky element as it scrolls underneath. Sticky elements need both a z-index and a background-color to layer correctly.
Fix: Add z-index: 100 (or appropriate value) and background-color to the sticky element.
Problem 5: Containing block ends too early
The sticky range is limited by the containing block. If the sticky element’s parent is too short, the element will stop sticking before you expect it to.
Fix: Ensure the sticky element’s parent spans the full desired sticky range.
Browser Compatibility
position: sticky is well-supported across all modern browsers:
| Browser | Support |
|---|---|
| Chrome | 56+ (full support) |
| Firefox | 32+ (full support) |
| Safari | 13+ (full unprefixed; -webkit-sticky from 6.1) |
| Edge | 16+ (full support) |
| iOS Safari | 13+ (full support) |
| Chrome Android | 56+ (full support) |
For production use, no prefix or polyfill is needed. Global browser support is above 97%.
Frequently Asked Questions
Why does position: sticky not work inside a flex or grid container?
Sticky works fine inside flex and grid containers, but you may need to set align-self: start (or align-items: start on the container) so the sticky item doesn’t stretch to the full row height. Without this, the containing block is the full row height and sticky appears not to work.
Can I use position: sticky with overflow: auto on the same element?
No. Setting both position: sticky and overflow: scroll|auto|hidden on the same element is self-defeating — the element becomes its own scroll container, and sticky has nowhere to scroll. Apply them to different elements.
Does position: sticky work inside CSS scroll snap containers?
Yes, sticky positioning works with scroll-snap-type containers. The sticky element snaps with the container’s scroll snap behavior while still sticking at its threshold within the snap container.
How do I make a sticky element with a top offset for a fixed navbar?
Set top to the height of the fixed navbar:
.sticky-section-header {
position: sticky;
top: 64px; /* matches your navbar height */
}
Can multiple sticky elements coexist on the same page?
Yes. Common use cases include a sticky site header plus sticky section headings. Each sticky element is independent and sticks at its own threshold. Use different z-index values to control which element appears on top when they overlap.
Is there a CSS variable approach for the sticky offset?
Yes — using CSS custom properties makes it easy to manage offsets consistently across a design system:
:root {
--nav-height: 64px;
}
.sticky-element {
position: sticky;
top: var(--nav-height);
}
All calculations happen in your browser. No data is sent to any server.