Table of contents
Skip table of contentsCSS's position: sticky
is a dream come true for web developers. It allows elements to switch between relative and fixed positioning based on the scroll position, without needing any JavaScript. It's great for persistent headers, sidebars or any element you want to keep in view while scrolling.
However, it can be surprisingly tricky to work with because it comes with a number of constraints, and you can end up in a situation where you think an element should be stuck, but it's just not working. In this post, we'll go over how position: sticky
can fail and how to fix these issues.
Positioning in CSS
The position
property tells you the type of positioning to use for an element, and you can think of those as "mini layout systems" that determine how elements are placed in relation to each other. There are four main values for the position
property:
static
: The default value. Elements are positioned according to the normal flow of the document.relative
: Elements are positioned relative to their normal position.absolute
: Elements are positioned relative to the offset parent (the nearest ancestor with a position other than static).fixed
: Elements are positioned relative to the viewport.
Position: sticky is the fifth one, and it's a hybrid of position: relative
and position: fixed
. It behaves like a relatively positioned element until it reaches a specified scroll position, at which point it becomes fixed. So it lets you switch between two mini layout systems, which means that you need to take the rules of both systems into account.
How sticky works
- Your element is relatively positioned to its normal position (meaning it's in the flow of the document) and scrolls along like any other element.
- It reaches a specific scroll position (defined by
top
,bottom
,left
, orright
), at which point it becomes fixed. - It stays fixed until it touches the bottom of its parent element (the parent element isn't stuck so still scrolls up), at which point it returns to its normal position.
What position:sticky needs to work
For position: sticky
to work, the browser needs to know when to switch between relative and fixed positioning.That specific scroll position is specified by inset
values: top
, right
, bottom
, and left
.
These inset value is measured from the nearest scrolling area, which usually is the viewport. A scrolling area is an element that has overflow
set to either scroll
or auto
(or hidden
, more on that later) and has an explicit height. The viewport is the visible part of the page in your browser, and it's a scrolling area by default.
That sticky element needs to be shorter than the scrolling area and shorter than its parent element (if there is a parent element between it and the scrolling area). If the sticky element is taller than the scrolling area, it will never be fully visible when reaching the inset value so can't get stuck and if it's the same height or taller than the parent element, that parent element will continue scrolling away, and the sticky element will scroll away with it.
- An element with
position: sticky
and aninset
value. - A scrolling area that the sticky element is in.
- The sticky element needs to be shorter than its parent element (if it has one).
- The sticky element needs to be shorter than the scrolling area.
Each of these requirements can be a source of failure, and we'll go over them in more detail.
Scrolling area issues
One of the most common reasons for position: sticky
to fail is related to its containing element, which is either the scrolling area that your element is in, or the viewport.
Issue: overflow: hidden
Overflow:hidden
creates a new scrolling area: one without a scroll bar but that can still be scrolled programmatically.
That means that if you have overflow:hidden
(or auto
or scroll
, for more explicit scrolling) on an ancestor of the sticky element it's now checking the inset values against that element instead of against the viewport, and they probably won't get stuck when you expect them to.
Note that when you set an overflow-x: hidden
somewhere, then it also forces overflow-y: auto
on the same element, so you still create a new scrolling area.
<div class="container">
<div class="sticky-element">I should be sticky</div>
</div>
.container {
overflow: visible; /* or clip */
}
Make sure you're not creating a new scrolling area between your sticky element and the viewport (unless you explicitly want that). If you really do need overflow: hidden
, switch it out to overflow: clip
, which hides the overflow but also disables programmatic scrolling. Learn more about that in Do you know about overflow:clip?.
Issue: no explicitly set height
An element can only scroll if it has a specified height, so when it doesn't, the browser can't determine when the sticky element should stick.
<div class="container">
<div class="sticky-element">I should be sticky</div>
</div>
.container {
height: 400px; /* or any appropriate height */
overflow-y: visible;
}
This also applies to any element that's between the sticky element and the scrolling area.
Your container should be taller than the sticky element. Without an explicit height it will be as high as the content inside of it. Sometimes that works when you have more than just the sticky element inside of it because the other elements inside the container cause it to be high enough for the sticky element to get stuck.
Sticky element issues
The sticky element itself can also be the source of positioning failures.
Issue: missing inset value
This might be a bit obvious, but it's easy to forget: a sticky element needs at least one of top
, bottom
, left
, or right
to be set so the browser knows where to stick it.
Failing example:
.sticky-element {
position: sticky;
/* Missing top, bottom, left, or right value */
}
Fix:
.sticky-element {
position: sticky;
top: 0; /* Add at least one of top, bottom, left, or right */
}
Issue: element larger than container (or viewport)
If the sticky element is larger than its container, it won't stick, because the sticky element needs to be fully visible before it can get stuck.
Failing example:
.sticky-element {
position: sticky;
top: 0;
height: 110%; /* Taller than container */
}
You can't always control how height an element is so if you can't change the height of the element, you can limit the height of the sticky element:
.sticky-element {
position: sticky;
top: 0;
max-height: 50%; /* Limit height to container */
overflow-y: auto; /* Allow scrolling within element if needed */
}
If you make it as high as the container then it will never stick, so you need to make sure it's shorter than the container for it to remain in view.
Flex and grid layout interactions
Flexbox and Grid layouts can introduce additional complexities when working with sticky positioning, because these layout systems also influence properties on the elements inside them.
Flexbox Issue
In a flex container, sticky elements may not stick as expected because a flex layout doesn't create a scrolling area by default (its height is dependent on its children). You need to set a height on the flex container and allow scrolling to make the sticky element work.
<div class="flex-container">
<div class="sticky-element">I should be sticky</div>
<div class="flex-item">Flex content</div>
</div>
.flex-container {
display: flex;
flex-direction: column;
height: 400px; /* Set a height on the flex container */
overflow-y: auto; /* Allow scrolling */
}
Grid and flex: align-items
Grid and flex layouts default to align-items: stretch
, which means that the sticky element is as high as the containing grid cell. That means that whenever it reaches the inset
and should get stuck, it's instantly unstuck again because the parent element scrolls away. You can fix this by setting align-items: start
on the grid container.
<div class="grid-container">
<div class="sticky-element">I should be sticky</div>
<div class="grid-item">Grid content 1</div>
<div class="grid-item">Grid content 2</div>
</div>
.grid-container {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
height: 400px;
overflow-y: auto;
align-items: start; /* This is the key change */
}
.sticky-element {
position: sticky;
top: 0;
height: auto; /* Allow the element to size to its content */
}
Alternatively, you can also set align-self: start
on the sticky element, if you want the other element to keep stretching.
Conclusion
position: sticky
is a powerful tool for creating sticky elements without JavaScript, but it has constraints that aren't immediately apparent. To make sure your sticky elements work as expected, you need to ensure that:
- The sticky element is in a scrolling area (and in the scrolling area you expect)
- The sticky element has an
inset
value. - The sticky element is shorter than the scrolling area.
By understanding these constraints, you can make sure that your sticky elements work as expected!