Search code examples
javascriptcssaddeventlistenersticky

scroll event listener + position: sticky element that changes height causes jittery infinite scroll


I have an element at the top of the screen with position:sticky; and a JS scroll eventlistener to add a stuck class when the element is stuck (scroll Y is bigger than 0).

This stuck makes an element (that has CSS transition) inside the sticky element to reduce in height.

When you scroll very slowly, this change in height makes the scrollY jump back to zero and removes the stuck class, and this creates a jolty scrolling loop which is infinite until you scroll faster and outscroll the issue.

How can I make this smooth when using position: sticky, you can see it live here:

https://jsfiddle.net/27rzba5v/


Solution

  • This is happening because you're changing the element's height, which is affecting the box model and thus the dimensions of your document. A simple fix would be to instead use a transform on .wrap instead of transitioning its height. It's better to animate transforms and opacity as it can be handled by the GPU rather than by making the browser repaint.

    var lastScrollY = 0;
    var ticking = false;
    
    window.addEventListener('scroll', function(e) {
    	lastScrollY = window.scrollY;
    	if ( ! ticking ) {
    		window.requestAnimationFrame(function() {
    			console.log( lastScrollY );
    			if ( lastScrollY > 0 ) {
    				document.body.classList.add('stuck');
    			} else {
    				document.body.classList.remove('stuck');
    			}
    			ticking = false;
    		});
    		ticking = true;
    	}
    } );
    body {
    	margin: 0;
    }
    .wrap {
    	background: #666;
    	text-align: center;
    	position: sticky;
    	top: 0;
      transition: 0.5s; /* Move transition here */
      transform-origin: top left; /* Make sure transition happens from the top left */
    }
    .block {
    	width: 80px;
    	height: 80px;
    	background: red;
    	display: block;
    }
    .stuck .wrap { /* Transform wrap instead of block */
    	transform: scaleY(.5); /* Change scale instead of height */
    }
    <div class="wrap">
    	<span class="block"></span>
    </div>
    
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque ac lorem a metus tincidunt eleifend id in odio. Nam malesuada hendrerit tristique. Pellentesque id ornare elit, ac lobortis metus. Sed iaculis et nisi et consectetur. Proin pellentesque metus mi, quis fringilla mauris pulvinar et. Vivamus pharetra elit ligula, eu consectetur magna consequat nec. Donec sed enim sit amet augue malesuada varius id a libero. Nullam nec ante id justo elementum congue quis in purus. Integer finibus cursus volutpat. Donec non laoreet ipsum.
    
    Maecenas id venenatis velit, eu feugiat dolor. Vestibulum malesuada erat ut turpis mattis vehicula. Vestibulum sem leo, cursus quis lacinia eu, tincidunt eu velit. Nulla odio elit, tristique vel bibendum vitae, placerat nec eros. Proin auctor id leo sed rutrum. Proin convallis erat sit amet neque aliquam vestibulum. Ut sodales vel nisl eu imperdiet.
    
    Donec a porttitor dui, vitae ullamcorper nibh. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nullam non elit eu elit blandit hendrerit id ut eros. Duis sagittis elementum ligula eu venenatis. Aenean nunc mauris, dignissim ut venenatis ac, pharetra eget magna. Curabitur elementum enim sed pharetra rhoncus. Praesent sodales at ex at consectetur. Curabitur sed dictum mi, ut eleifend arcu. Nam feugiat risus quis congue porttitor. Maecenas vehicula lorem ultrices ante sollicitudin, placerat sollicitudin dolor fermentum. Aliquam arcu turpis, faucibus vel placerat a, molestie vel lectus. Sed condimentum euismod tincidunt. Mauris odio tortor, luctus id eleifend vitae, aliquet ut libero. Vestibulum vitae placerat turpis. Duis nec facilisis eros. Morbi ipsum arcu, tempus ac massa nec, mollis aliquet lacus.
    
    Phasellus maximus eros quis massa maximus, et mattis tellus cursus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras id lacus interdum, cursus diam ut, posuere purus. Duis non vestibulum nulla, vitae egestas elit. Phasellus venenatis libero in nunc lobortis tempor in vel orci. Praesent varius lacus eleifend, pretium lectus ut, pulvinar arcu. Nunc ornare dolor velit, id pulvinar urna semper eget. Aenean finibus dui vitae dolor ullamcorper finibus. Cras consequat viverra tellus, a accumsan sem viverra sed. Nulla felis tortor, laoreet non tincidunt elementum, tempus ac risus. Sed nibh nisl, ultrices vel iaculis in, fringilla at ipsum. Curabitur metus ligula, tincidunt non arcu eget, ultricies pellentesque ligula. Duis id est posuere, molestie urna non, gravida tortor. Praesent lacinia erat ac diam sagittis, quis faucibus nunc blandit.
    
    Fusce aliquet tincidunt turpis, ut consequat metus consectetur at. Nunc volutpat semper enim, ut finibus orci luctus in. Mauris pharetra consectetur arcu sed pulvinar. Sed cursus fermentum velit, mattis varius ante commodo ac. Fusce erat mauris, sagittis quis eros vitae, consectetur venenatis nibh. Fusce porttitor tortor lectus, at efficitur diam dictum et. Quisque et suscipit sem. Mauris vulputate orci tellus, non efficitur eros blandit ut. Donec eget hendrerit elit.

    If you must transition the element's height then you could offset the effects of the dimensions changing by using a container as a buffer.

    var lastScrollY = 0;
    var ticking = false;
    
    window.addEventListener('scroll', function(e) {
    	lastScrollY = window.scrollY;
    	if ( ! ticking ) {
    		window.requestAnimationFrame(function() {
    			console.log( lastScrollY );
    			if ( lastScrollY > 0 ) {
    				document.body.classList.add('stuck');
    			} else {
    				document.body.classList.remove('stuck');
    			}
    			ticking = false;
    		});
    		ticking = true;
    	}
    } );
    body {
    	margin: 0;
    }
    .wrapoffset { /* Make this element sticky instead */
      height: 80px;
      position: sticky;
      top: 0;
      width: 100%;
    }
    .wrap {
    	background: #666;
    	text-align: center;
      width: 100%;
    }
    .block {
      height: 80px;
    	width: 80px;
    	background: red;
    	display: block;
    	transition: 0.5s;
    }
    .stuck .block {
    	height: 40px;
    }
    <div class="wrapoffset"> <!-- new element -->
      <div class="wrap">
        <span class="block"></span>
      </div>
    </div>
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque ac lorem a metus tincidunt eleifend id in odio. Nam malesuada hendrerit tristique. Pellentesque id ornare elit, ac lobortis metus. Sed iaculis et nisi et consectetur. Proin pellentesque metus mi, quis fringilla mauris pulvinar et. Vivamus pharetra elit ligula, eu consectetur magna consequat nec. Donec sed enim sit amet augue malesuada varius id a libero. Nullam nec ante id justo elementum congue quis in purus. Integer finibus cursus volutpat. Donec non laoreet ipsum.
    
    Maecenas id venenatis velit, eu feugiat dolor. Vestibulum malesuada erat ut turpis mattis vehicula. Vestibulum sem leo, cursus quis lacinia eu, tincidunt eu velit. Nulla odio elit, tristique vel bibendum vitae, placerat nec eros. Proin auctor id leo sed rutrum. Proin convallis erat sit amet neque aliquam vestibulum. Ut sodales vel nisl eu imperdiet.
    
    Donec a porttitor dui, vitae ullamcorper nibh. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nullam non elit eu elit blandit hendrerit id ut eros. Duis sagittis elementum ligula eu venenatis. Aenean nunc mauris, dignissim ut venenatis ac, pharetra eget magna. Curabitur elementum enim sed pharetra rhoncus. Praesent sodales at ex at consectetur. Curabitur sed dictum mi, ut eleifend arcu. Nam feugiat risus quis congue porttitor. Maecenas vehicula lorem ultrices ante sollicitudin, placerat sollicitudin dolor fermentum. Aliquam arcu turpis, faucibus vel placerat a, molestie vel lectus. Sed condimentum euismod tincidunt. Mauris odio tortor, luctus id eleifend vitae, aliquet ut libero. Vestibulum vitae placerat turpis. Duis nec facilisis eros. Morbi ipsum arcu, tempus ac massa nec, mollis aliquet lacus.
    
    Phasellus maximus eros quis massa maximus, et mattis tellus cursus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras id lacus interdum, cursus diam ut, posuere purus. Duis non vestibulum nulla, vitae egestas elit. Phasellus venenatis libero in nunc lobortis tempor in vel orci. Praesent varius lacus eleifend, pretium lectus ut, pulvinar arcu. Nunc ornare dolor velit, id pulvinar urna semper eget. Aenean finibus dui vitae dolor ullamcorper finibus. Cras consequat viverra tellus, a accumsan sem viverra sed. Nulla felis tortor, laoreet non tincidunt elementum, tempus ac risus. Sed nibh nisl, ultrices vel iaculis in, fringilla at ipsum. Curabitur metus ligula, tincidunt non arcu eget, ultricies pellentesque ligula. Duis id est posuere, molestie urna non, gravida tortor. Praesent lacinia erat ac diam sagittis, quis faucibus nunc blandit.
    
    Fusce aliquet tincidunt turpis, ut consequat metus consectetur at. Nunc volutpat semper enim, ut finibus orci luctus in. Mauris pharetra consectetur arcu sed pulvinar. Sed cursus fermentum velit, mattis varius ante commodo ac. Fusce erat mauris, sagittis quis eros vitae, consectetur venenatis nibh. Fusce porttitor tortor lectus, at efficitur diam dictum et. Quisque et suscipit sem. Mauris vulputate orci tellus, non efficitur eros blandit ut. Donec eget hendrerit elit.

    If you want to optimize the performance even more you can look into using IntersectionObserver with a polyfill instead of listening for events on scroll.