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.
[](https://training.leftlogic.com/buy/terminal/cli2?coupon=BLOG\&utm_source=blog\&utm_medium=banner\&utm_campaign=remysharp-discount)
[READER DISCOUNTSave $50 on terminal.training](https://training.leftlogic.com/buy/terminal/cli2?coupon=BLOG\&utm_source=blog\&utm_medium=banner\&utm_campaign=remysharp-discount)
[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:
-
Runs a jQuery selector every time the scroll event fires
-
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]()

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);

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); }

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.

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);

Vitali
0 points
6 years ago
You are risking to have even less browser support [http://caniuse.com/#search=…;](http://caniuse.com/#search=classlist)

rem
0 points
6 years ago
Good shout, I always forget there’s a 2nd arg to toggle.

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

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!

rem
0 points
6 years ago
I’ve already posted them on my blog, so please do enjoy the next parts 😀

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)

rem
0 points
6 years ago
3 parter, I’ll get to that eventually 👍😁

Anthony Ricaud
0 points
6 years ago
Good teaser! :)
[Commento](https://commento.io)