CSS “position: sticky” – Introduction and Polyfills

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.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.technbuzz.com/ Samiullah Khan

    By there way, the code in the javascript solutions are very robust, great use of booleans

  • LouisLazaris

    You’re right. Of course, this applies to position: fixed in general, and has always been a problem, and is not specific just to “sticky”.

    The problem is, for the browser to “reserve” the viewport space, it would have to correctly identify the height of that element. Probably that’s not hard to do, but it would seem odd if the browser did that, calculating an arbitrary height value. I don’t know, maybe it’s easy to do in the browser, but it doesn’t feel like the kind of thing that browser’s should be doing, as helpful as it might be.

    • Yo Dirkx

      Of course the browser should do this. What else is the point of anchors on a page with a sticky anything? I’m amazed the browser doesn’t do this. It would be the one thing we can’t fake easily. No, JS isn’t good enough, there are too many issues and scenario’s with anchors and scroll positions.

  • http://ChiefAlchemist.com/ Mark Simchock

    Great point. Kudos.

    I want to say I saw a javascript snippet for this. But don’t hold me to that. In any case, it certainly would make sense to have some sort of offset for anchors. Odd that after all this time there’s not.

  • http://stephenmorley.org/ Stephen Morley

    Depending on which elements are being used as link targets, this can be corrected with a little CSS. For example, on one of my sites each h2 element is a link target, and the stylesheet sets top padding and corresponding negative top margin. This means the top of the element is moved further up the page relative to its content, and so the sticky navigation covers the additional padding rather than the content.

  • Aurelio De Rosa

    Hi.

    Yes, Louis and I are aware of this message. In fact, in the article you can read the note regarding the compatibility with Chrome 38.

  • Aurelio De Rosa

    If you intend to set “position: absolute” to the element that should stick, this won’t make it. You need to apply “position: fixed” to simulate the effect of “sticky”.

  • http://aarontgrogg.com/ Aaron T. Grogg

    This is easily offset by switching the margin/padding on the headers and p-tags, so the anchor elements have a top-padding sufficient to push them below the sticky header. Not an ultimate solution, but if you knew you were doing sticky, would be easy to set-up from the start.