CSS “position: sticky” – Introduction and Polyfills

Aurelio De Rosa
Share

If you’ve read the article Obvious Design always wins by Annarita Tranfici, you might agree with her statement:

People expect to see common patterns: find the main menu in the top of the page, a search box at the top-right corner, a footer at the bottom, and so on.

I agree that people expect certain components of a website to be placed in a particular location and, in my opinion, this is even more true when it comes to the main menu.

Sometimes, due to a client request or because we have determined this is the best approach, we may require that a main navigation stay visible at all times on the page, without it being fixed in place, essentially following the page content. In recent years, a lot of JavaScript-based solutions have seen the light of day because CSS alone was unable to achieve this task.

In this article we’ll discuss position: sticky, the new CSS solution to this problem.

What Problem are We Solving?

Before discussing this new value for the position property, let’s better understand what is the problem we’re trying to solve.

Let’s pretend the main menu of our amazing website is right after the header but still at the top of the page (not in a sidebar) and that it occupies all the width available. This might look like this:

See the Pen Example with no sticky menu by SitePoint (@SitePoint) on CodePen.

What we want to achieve is that when the user scrolls the page, as soon as the menu is positioned at the top of the viewport, instead of the menu scrolling out of view, it will stick at the top position — as if it had a position: fixed applied to it (only when it reaches the top of the viewport).

To achieve this with traditional code we need to add some JavaScript. We listen for the scroll event of the page and use JavaScript to change the value of the position and top properties according to the current position of the viewport. Specifically, we need to add top: 0 and position: fixed to the menu when it’s at the top of the viewport, and then revert the properties back to their defaults otherwise.

An alternative, but similar, approach is to create a class in our CSS where those values are present, and then add and remove the class as needed, with JavaScript, which might look like this:

var menu = document.querySelector('.menu')
var menuPosition = menu.getBoundingClientRect().top;
window.addEventListener('scroll', function() {
    if (window.pageYOffset >= menuPosition) {
        menu.style.position = 'fixed';
        menu.style.top = '0px';
    } else {
        menu.style.position = 'static';
        menu.style.top = '';
    }
});

Please note that this snippet doesn’t deal with older versions of Internet Explorer. If you need to deal with those browsers (poor you!), I’ll provide a few polyfill options that you can consider.

A live demo of this second step is shown below:

See the Pen position: sticky with Simply JS by SitePoint (@SitePoint) on CodePen.

But wait! Can you spot the issue this code causes? Many implementations I’ve seen, including the one we’ve developed so far, don’t take into account an important issue. When we change the position of the element to fixed, it leaves the flow of the page, so the elements below it “jump up” by a number of pixels roughly equal to the height of the element (the height of this “jump” depends on margins, borders, and so on).

A possible solution is to inject a placeholder element that is same size as the one we want to “stick” so that when we update the sticky element’s style, there won’t be a jump. In addition, we don’t want to reassign the values over and over again for no reason if the right values are already set. Finally, we want to employ the technique I described using a CSS class.

The final version of the JavaScript code is listed below:

var menu = document.querySelector('.menu');
var menuPosition = menu.getBoundingClientRect();
var placeholder = document.createElement('div');
placeholder.style.width = menuPosition.width + 'px';
placeholder.style.height = menuPosition.height + 'px';
var isAdded = false;

window.addEventListener('scroll', function() {
    if (window.pageYOffset >= menuPosition.top && !isAdded) {
        menu.classList.add('sticky');
        menu.parentNode.insertBefore(placeholder, menu);
        isAdded = true;
    } else if (window.pageYOffset < menuPosition.top && isAdded) {
        menu.classList.remove('sticky');
        menu.parentNode.removeChild(placeholder);
        isAdded = false;
    }
});

And this is the declaration block for the sticky class:

.sticky {
    top: 0;
    position: fixed;
}

The final result is shown in this next demo:

See the Pen position: sticky with JS, improved by SitePoint (@SitePoint) on CodePen.

Now that you have a good grasp of what the problem is and what’s a possible JavaScript-based solution, it’s time to embrace modernity and discuss what this position: sticky is all about.

What’s position: sticky?

If you’ve been so brave to carefully follow the previous section, you may be wondering “Why can’t the browsers do this for me?” Glad you asked!

sticky is a new value introduced for the CSS position property. This value is supposed to behave like position: static within its parent until a given offset threshold is reached, in which case it acts as if the value was fixed. In other words, by employing position: sticky we can solve the issue discussed in the previous section without JavaScript.

Recalling our previous example and using this new value we can write:

.menu {
    margin: 0;
    padding: 0;
    width: 100%;
    background-color: #bffff3;
    position: sticky;
}

And the browser will do the rest! It’s that easy.

Browser Support and Polyfills

The support for this new value is pretty poor at the moment. Here’s how things stack up for each browser:

  • Firefox 26+ – Supported by setting css.sticky.enabled to “true” under about:config.
  • Chrome 23+ – Supported by enabling “experimental Web Platform features” in chrome://flags.
  • Chrome 38(?) – The Chrome team have recently removed this feature from Blink, so it’s currently not available in Chrome Canary (version 38.x), even with the flag. You can read the bug report explaining the removal, but we suspect the feature will be re-implemented shortly, and possibly without a disruption in the stable version’s support.
  • Safari 6.1+ – Supported using the -webkit vendor prefix on the value (i.e. position: -webkit-sticky)
  • Opera 23+ – Supported by enabling “experimental Web Platform features” in about:flags.
  • Internet Explorer – No support (see status)

See position: sticky on Can I Use… for all the details.

Fortunately there a number of polyfills to choose from:

Demo

The following demo shows position: sticky in action. As mentioned, to see it work, and depending on the browser you’re using, you many need to activate a flag.

See the Pen position: sticky (pure CSS version) by SitePoint (@SitePoint) on CodePen.

Conclusion

Although the new feature doesn’t have great browser support, and you might be hesitant to polyfill it, it will degrade gracefully to the default position: static or position: fixed, if you define an alternative style using Modernizr.

If you’ve experimented with this property or know of any other polyfills, let us know in the comments.