Finding an optimal sticky element solution

Finding an optimal sticky element solution

A sticky element is a page element that stays fixed on the screen as you scroll, but is restricted by its parent element: when parent scrolls out of viewport, so does the sticky element.

position: sticky

The standard way of implementing sticky elements is with position: sticky.

Here's a simple demo:

normal sticky demo

The above is implemented with just a few lines of CSS:

main {  
  display     : flex;
  align-items : flex-start;

.sticky {
  flex-shrink : 0;
  position    : sticky;
  top         : 0;
  width       : 10rem;
  height      : 10rem;

Browser support for position: sticky is at 68% (as of May 2017), which is not great, but it falls back gracefully to static positioning.

Unfortunately, position: sticky has two flaws that forced us to look for a different solution.

position: sticky flaw 1: does not work inside overflow: hidden!

When I first encountered this problem, it took me quite a while to figure out. Turns out, it's officially not supported.

The spec defines a stickily positioned box as positioned similarly to a relatively positioned box, but the offset is computed with reference to the nearest ancestor with a scrolling box, or the viewport if no ancestor has a scrolling box.

Unfortunately, a parent with overflow: hidden qualifies for having a "scrolling box".

Thus, if your layout uses overflow: hidden on a container, position: sticky won't work for you. It will behave as position: relative.

position: sticky flaw 2: viewing the bottom of a sticky element that's taller than viewport

In certain use cases, a sticky element can be taller than viewport. position: sticky will prevent the user from seeing the bottom of the sticky element until they scroll to the bottom of its parent:

sticky taller than viewport

This behavior is counter-intuitive and counter-productive.

I believe, the optimal behavior for a taller-than-viewport sticky element can be described with two simple rules:

When the user scrolls down, the sticky element should scroll along with the parent until its bottom is visible. When the bottom reaches the viewport, the element should stick.

Accordingly, when the user scrolls up, the sticky element should sticky only when its top reaches the viewport. While the top of the sticky element is above viewport, it should scroll with the parent.

Looking at polyfills

There are quite a number of position: sticky polyfills and standalone solutions with extra features (e. g. multiple non-overlapping sticky elements in a column).

All those solutions rely on position: fixed to mimic the stuck mode of the sticky element. I've tried a few, and all of them required extra work for the sticky element to behave as expected.

Unfortunately, the position: fixed approach also has fundamental flaws.

position: fixed flaw 1: does not work inside transform-ed parent

Just like overflow: hidden cancels out position: sticky, a parent with CSS transform nullifies position: fixed on children. The child behaves as a static element.

According to the spec, elements with transforms act as a containing block for fixed position descendants, so position: fixed under something with a transform no longer has fixed behavior.

position: fixed flaw 2: child overflows out of a parent with overflow: scroll

This issue happens under these conditions:

  • you must use a custom scrolling parent instead of global window scrolling;
  • the scrolling parent must be shorter than the viewport.

This combination of conditions is common for UIs that mimic desktop apps: instead of a global page scrolling they split the screen into multiple areas, and each area may have its own scrolling.

Now, if a sticky element is taller than its scroll parent, it will pop out and overflow, disrespecting parent boundaries.

For this reason, some sticky polyfills don't support custom element scrolling at all.

Take leafo/sticky-kit for example. Its demo page makes an impression that it doesn't have this issue. But if you inspect the HTML structure, you'll discover that it uses <iframe>s to bind the fixed element inside a container — a technique you will never use in an actual webapp.

A custom solution using transform: translate

To avoid all of the issues mentioned above, I've implemented a custom solution that uses a simple idea: use transform: translate to position the sticky element.

Sticky elements taller than viewport work naturally:


And it is restricted by neither overflow: hidden nor transform on parent elements.

Unfortunately, this approach is a compromise with its own flaws.

  1. Chrome throttles window scrolling, making the sticky element jitter during scrolling. This doesn't happen in Firefox and it does not happen in Chrome with a custom scroll container.

  2. The position of sticky elements has to be continuously recalculated as you scroll, making scrolling feel laggy on slow systems.

To work around the performance issue, recalculation can be throttled. Throttling will cause the sticky element to be jerky. To avoid that, CSS transition can be applied:

sliding-sticky animated

This makes scrolling fast and smooth, but the animation may appear obtrusive to some users.

This implementation is available as an addon for Ember (demo).

ITs logic is jQuery-based and can be effortlessly extracted into an app based on any framework.


Up to this point, I've used the word unfortunately four times already.

Unfortunately, this means that the sticky element problem has no ideal solution. Now you know available options, their pros and cons.

position: sticky position: fixed-based polyfills transform: translate transform: translate with trottling & animation
Ease of use CSS-only Requires JS Requires JS Requires JS
Browser support (May 2017) 68% 95% 94% 94%
Works inside overflow: hidden No Yes Yes Yes
Works nicely with sticky elements taller than viewport No Most do Yes Yes
Works inside `transform`-ed parent Yes No Yes Yes
Works with a custom scroll parent when sticky is taller Yes No Yes Yes
Performance penalty No No Yes No
Obtrusive animation No No No Yes

At Deveo, we do have a custom scroll container, but its height is equal to viewport. We use sticky-kit, patched to support custom element scrolling.

Seamless software development.

Code management and collaboration platform with Git, Subversion, and Mercurial.

Sign up for free
comments powered by Disqus