Sticky Elements with CSS position:sticky

CSS continues to improve by adding new features that are usually needed by developers. Those features like animations for example, reduce the amount of HTTP requests, dependency on somebody’s code, and the amount of code needed to provide a similar user experience as we did with JavaScript. The good thing about CSS is that it not only reduces the amount of code, but we don’t have to depend totally on JavaScript in order to improve our user interfaces. For example, the sticky navigation bar.

The position sticky is very popular with navigation as the sticky navigations are quicker to navigate and are always available no matter what part of the page is being shown to the user, developers have adopted this approach in a lot of websites. So, with a single line of CSS, we can create the same effect. The only other property needed is the top position, which tells the browser the distance from the top to become sticky. Top:0 waits until the element touches the top edge of the window in order to become sticky.

selector {
	position:sticky;
	top:0;
}

he position sticky is a combination of position static and fixed. The position of the element is static until on scroll, the element reaches the top of the screen when its position changes to fixed. The only issue with this feature is that it is not supported by all major browsers yet. However, it won’t affect your design because browsers that don’t support it, will just fallback to the static position.

If you really need to apply the sticky to all major browsers without having to wait for them to support it, I recommend a polyfill. However, I am still surprised that this has not been implemented in all modern browsers yet even though it has been proposed for years. At the moment of this writing, only Firefox and Safari have the sticky position. The status of Opera and Safari are in development, while Edge has it under consideration. In case you want to implement it, you can use a script similar to this one until sticky is supported everywhere.

;(function() {

    //Checks if position:sticky is supported
    function isStickySupported() {
        let div = document.createElement('div');
        div.style.position = 'sticky';
        return div.style.position === 'sticky';
    };

    //If position:stiky is supported, no need for the script
    if (isStickySupported()) {
        return;
    }

    //Get the computed style of a given element
    function getCSS(prop, el) {
        return window.getComputedStyle(el, null).getPropertyValue(prop);
    }

    //Clone the element and append it right after the original node
    //That way, it will leave the empty space of the node when sticky
    function cloneElement(el) {
        var clone = el.cloneNode(true);
        clone.setAttribute('stiky-space', 'true');
        clone.style.display = 'none';
        clone.style.visibility = 'hidden';
        el.parentNode.insertBefore(clone, el.nextSibling);
    }

    //Array of all .sticky elements on the page
    // Array.prototype.slice used to convert the nodeList into Array for browsers that don't support it
    let stickies = Array.prototype.slice.call(document.querySelectorAll('.sticky'), 0);

    //Foreach .sticky get the position on screen and make it an attribute for future calculations
    stickies.forEach(function(sticky) {
        sticky.setAttribute('data-sticky-top', getCSS('top', sticky));
        sticky.setAttribute('data-sticky-origin', sticky.getBoundingClientRect().top);
        cloneElement(sticky);
    });

    //On Scroll, it checks the position of the elements and simulates the sticky position
    //It also adds a class stuck when sticky is active and removes the class once the element is
    //back to static position
    function stickify() {
        let pageYOffset = window.pageYOffset;

        stickies.forEach(function(sticky) {

            let top = sticky.getBoundingClientRect().top,
                width = getCSS('width', sticky),
                dataTop = parseInt(sticky.getAttribute('data-sticky-top')),
                origin = parseInt(sticky.getAttribute('data-sticky-origin')),
                stickySpace = sticky.nextElementSibling;

            if (top <= dataTop) {
                sticky.style.position = 'fixed';
                sticky.style.top = top + 'px';
                sticky.classList.add('stuck');
                sticky.style.width = width;
                stickySpace.style.display = '';

            } else if (pageYOffset < origin) {

                sticky.style.position = 'static';
                sticky.classList.remove('stuck');
                stickySpace.style.display = 'none';
            }
        });
    }
    
    window.addEventListener('scroll', stickify);
    window.addEventListener('load', stickify);
    //On refresh set scroll to the top so the script can perform the calculations
    //of the positions of the elemements
    window.addEventListener('beforeunload', function() {
        window.scrollTo(0, 0);
    });

})();

In case you are interested, there are several pollyfills avaible like stickyfill and fixed-sticky that you can use instead of creating your own script.

Teylor Feliz
Teylor Feliz

Teylor is a seasoned generalist that enjoys learning new things. He has over 20 years of experience wearing different hats that include software engineer, UX designer, full-stack developer, web designer, data analyst, database administrator, and others. He is the founder of Haketi, a small firm that provides services in design, development, and consulting.

Over the last ten years, he has taught hundreds of students at an undergraduate and graduate levels. He loves teaching and mentoring new designers and developers to navigate the rapid changing field of UX design and engineering.

Articles: 182