I have mixed feelings about sticky headers on web pages, but it annoys me more when the implementation causes scroll jank or contributes to it.

The coding pattern needs a few small changes. I’m also posting this under: stuff that’s been said before, but is worth saying again.

[I’ve published 38 videos for new developers, designers, UX, UI, product owners and anyone who needs to conquer the command line today.](https://training.leftlogic.com/buy/terminal/cli2?coupon=BLOG\&utm_source=blog\&utm_medium=banner\&utm_campaign=remysharp-discount)

The sample starting point[](#the-sample-starting-point)

I recently bought an HTML single page template for a project I’m working on, and have been slowly making my way through the code tweaking it to my preferences, when I saw this:

var toggleHeaderFloating = function() {
  // Floating Header
  if ( $window.scrollTop() > 80 ) {
    $( '.header-section' ).addClass( 'floating' );
  } else {
    $( '.header-section' ).removeClass( 'floating' );
  };
};

$window.on( 'scroll', toggleHeaderFloating );

The code will check on every scroll tick whether the scroll position is over 80 pixels, and if it is, it’ll add a class (that "floats" the header section) or it will remove the class.

It’s fair to assume that $window is a jQuery instance of the window object. However, there’s a whole bunch of no-nos going on in this code for me. It’s not a big deal, but understanding the red flags helps us to understand how to avoid little snags in the future.

Do nothing-to-nothing on scroll[](#do-nothing-to-nothing-on-scroll)

I can’t find the original post, but Paul Irish, some many years ago shared insights into scrolling performance, and recommended that inside of the scroll event (and likely also applies to wheel and probably mousemove), that you should avoid touching the DOM and avoid triggering layout (also known as reflows). Paul also collected an excellent list of [what triggers layout](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) which is brief enough for me to remember.

If we’re doing nothing inside a scroll event, what can we do? We can debounce, using requestAnimationFrame. When the user scrolls, we’ll schedule a function that will check the scroll position, but if the user is scrolling quickly, then that action will take priority, and ideally avoid scroll-jank:

// used to only run on raf call
var rafTimer;

$window.on('scroll', function () {
  cancelAnimationFrame(rafTimer);
  rafTimer = requestAnimationFrame(toggleHeaderFloating);
});

If you need to support IE9 and below (which for this, I’d recommend just not having the sticky header at all), you can use [Paul’s polyfill for rAF from 2011](http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/).

jQuery selecting[](#jquery-selecting)

The original code does two things that bothers me:

  1. Runs a jQuery selector every time the scroll event fires

  2. Queries every element

Admittedly getElementsByClassName (which jQuery/sizzle [uses if the selector is a single class](https://github.com/jquery/jquery/blob/b14ce54334a568eaaa107be4c441660a57c3db24/external/sizzle/dist/sizzle.js#L262-L265)) is pretty well optimised, so it’s not of great concern. However, we don’t need to construct a new jQuery object on every scroll tick.

For an idea of the number of times that code would be run, in Chrome devtools put this the console and scroll the page:

window.onscroll = () => console.count('scroll')
// or monitorEvents('scroll')

Let’s cache:

var $headerSection = $('.header-section');
var toggleHeaderFloating = function() {
  // Floating Header
  if ( $window.scrollTop() > 80 ) {
    $headerSection.addClass( 'floating' );
  } else {
    $headerSection.removeClass( 'floating' );
  };
};

var rafTimer;
$window.on('scroll', function () {
  cancelAnimationFrame(rafTimer);
  rafTimer = requestAnimationFrame(toggleHeaderFloating);
});

Only change the class once[](#only-change-the-class-once)

The class on the header section really only needs changing in one scenario: when the scroll position goes over a certain threshold.

The alternative is to use classList as it’s optimised to check whether the class needs changing before it touches the DOM.

Here’s my (vanilla) version that I use on the [ffconf 2016 conference site](https://2016.ffconf.org):

var rafTimer;
window.onscroll = function (event) {
  cancelAnimationFrame(rafTimer);
  rafTimer = requestAnimationFrame(toggleHeaderFloating);
};

function toggleHeaderFloating() {
  // does cause layout/reflow: https://git.io/vQCMn
  if (window.scrollY > 80) {
    document.body.classList.add('sticky');
  } else {
    document.body.classList.remove('sticky');
  }
}

In the next part, I’ll share how I combined this technique with smooth scrolling.

Published 28-Jun 2017 under #web. [Edit this post](https://github.com/remy/remysharp.com/blob/main/public/blog/sticky-headers.md)

Comments

Lock Thread

Login

Add Comment[M ↓   Markdown]()

[Upvotes]()[Newest]()[Oldest]()

![](/images/no-user.svg)

TheVortexx77

0 points

6 years ago

What about the Idea using a debounce Function to imits the rate at which a function can fire\ var navigationObserver = debounce(function () {\ if ($window\.scrollTop() > 70) {\ $header.addClass('scrolling');\ } else {\ $header.removeClass('scrolling');\ }\ }, 100);\ $window\.on('scroll', navigationObserver);

![](/images/no-user.svg)

purp

0 points

6 years ago

classList as it’s optimised to check whether the class needs changing before it touches the DOM

The same is true of jQuery’s addClass() (already in use in your "before" example):

// Only assign if different to avoid unneeded rendering. finalValue = stripAndCollapse(cur); if (curValue !== finalValue) { elem.setAttribute("class", finalValue); }

![](/images/no-user.svg)

rem

0 points

6 years ago

Not sure if that’s entirely correct. Only because jQuery will look up the className before deciding whether to add it. This lookup is mildly more costly that a native classList native call. But we’re talking micro optimisations at this point.

![](/images/no-user.svg)

Christian Sonne

0 points

6 years ago

If you’re using classList, then you might even want to avoid the if/else entirely and just do

document.body.classList.toggle('sticky', window\.scrollY > 80);

![](/images/no-user.svg)

Vitali

0 points

6 years ago

You are risking to have even less browser support [http://caniuse.com/#search=…​;](http://caniuse.com/#search=classlist)

![](/images/no-user.svg)

rem

0 points

6 years ago

Good shout, I always forget there’s a 2nd arg to toggle.

![](/images/no-user.svg)

Damian Saunders

0 points

6 years ago

Hi Remy,\ Thank you for this article, I’ve been struggling with this stuff myself (limited understanding) so you’re really helping in a big way!!

One thing I’m curious about: what method, if any, do you use to determine whether you’re on an IOS, or mobile device and not apply the sticky header?

Cheers\ Damian

![](/images/no-user.svg)

Jon R. Humphrey

0 points

6 years ago

Remy,\ I’ve been dealing with this myself on the latest version of my site design so I’m really looking forward to the rest of your solution!\ Ta!

![](/images/no-user.svg)

rem

0 points

6 years ago

I’ve already posted them on my blog, so please do enjoy the next parts 😀

![](/images/no-user.svg)

Anthony Ricaud

0 points

6 years ago

When sticky headers have no style changes, the recent position: sticky

in CSS is a nice alternative to writing and executing JavaScript. [It does not work in IE but will in the next version of Edge.](https://caniuse.com/#search=sticky)

![](/images/no-user.svg)

rem

0 points

6 years ago

3 parter, I’ll get to that eventually 👍😁

![](/images/no-user.svg)

Anthony Ricaud

0 points

6 years ago

Good teaser! :)

[Commento](https://commento.io)