Adding Headroom with JavaScript

Internet Browser
Image by janjf93 from Pixabay

Fixed navigation bars have an annoying drawback — they take up precious screen real estate. We can add headroom to any web page by automatically retracting and expanding the navigation bar on Of course, retracting and expanding navigation bars can be even more annoying. In the Improvements section we try to ease this pain.

We will create a super simple implementation with vanilla JavaScript and discuss some ways to improve its behavior. First let’s add some hacker style boilerplate to our app.js file.

/**
 * Boilerplate Functions
 */
function posf (f, a) { for (var i=0; i < a.length; i++) { if (f(a[i])) return i; } return -1; }
function apos (x, a) { return (typeof x == 'function') ? posf(x,a) : Array.prototype.indexOf.call(a,x) }
function arem (a, x) { var i = apos(x, a); if (i >= 0) { a.splice(i, 1); } return a; }
function afind (x, a) { var i = apos(x, a); return (i >= 0) ? a[i] : null; }
function addClass (el, cl) { if (el) { var a = el.className.split(' '); if (!afind(cl, a)) { a.unshift(cl); el.className = a.join(' ')}} }
function remClass (el, cl) { if (el) { var a = el.className.split(' '); arem(a, cl); el.className = a.join(' ') } }

Then setup our variables. The variable previousPosition stores the initial page position. Bind to our navigation bar using the navbar selector and grab its height using offsetHeight.

/**
 * Setup Variables
 */
var previousPosition = window.pageYOffset;
var navbar = document.getElementById("navbar");
var navbarHeight = navbar.offsetHeight;

Add an onscroll event with a simple check that hides the navigation bar on scroll down and reveals it on scroll up.

/**
 * Headroom Action
 */
window.onscroll = function() {
  var currentPosition = window.pageYOffset;

  if (previousPosition > currentPosition) {
    remClass(navbar, 'headroom');
  } else if (currentPosition > navbarHeight) {
    addClass(navbar, 'headroom');
  }

  previousPosition = currentPosition;
};

We add headroom to the page only when the current position is greater than the navigation bar’s height.

else if (currentPosition > navbarHeight) {
  addClass(navbar, 'headroom');
}

For our CSS style sheet, force the navigation bar upwards from its initial fixed position and give it a transition time to make the movement less jarring.

.headroom { top: -5em !important; }
.navbar {
  -webkit-transition: top 0.75s; 
  -o-transition: top 0.75s; 
  transition: top 0.75s; 
}

Demonstration and Source

Improvements

How can we improve this simple script? Let’s introduce velocity. We will show the navigation bar only if the user scrolls up faster than normal.

The velocity will be the difference between the previous and current position. The greater the difference, the faster the user scrolls. A scroll downwards produces a negative value and a scroll upwards produces a positive value.

var velocity = previousPosition - currentPosition;

Now that we know the velocity, we can add resistance by setting a velocity threshold of 55. The user must now overcome this velocity to reveal the navigation bar. The effect of this resistance threshold will vary based on device and browser.

The else if condition is capped to velocity < 0. This handles a situation where the user performs a flick scroll gesture which involves both acceleration and deceleration. In this scenario accidental triggers can happen if we do not consider the direction of the scroll.

if (velocity > 55 || currentPosition < navbarHeight) {
  remClass(navbar, 'headroom');
} else if (velocity < 0) {
  addClass(navbar, 'headroom');
}

Due to this implementation, add an initial check at run time to handle a refresh anywhere in the middle of the This is an unnecessary check, but I did fancy the initial auto-hide on page load effect.

var previousPosition = window.pageYOffset;
var navbar = document.getElementById("navbar");
var navbarHeight = navbar.offsetHeight;

if (previousPosition > navbarHeight) {
  addClass(navbar, 'headroom');
}

window.onscroll = function() {
  var currentPosition = window.pageYOffset;
  var velocity = previousPosition - currentPosition;

  if (velocity > 55 || currentPosition < navbarHeight) {
    remClass(navbar, 'headroom');
  } else if (velocity < 0) {
    addClass(navbar, 'headroom');
  }
 
  previousPosition = currentPosition;
};

Demonstration and Source

Conclusion

That’s it. We can make even more improvements but this looks good enough.

Updated 8 April 2019