+++ date = "2019-04-08T16:45:37+00:00" publishdate = "2023-12-29T07:08:55+00:00" title = "Adding Headroom with JavaScript" slug = "adding-headroom-with-javascript" author = "Thedro" tags = ["javascript","webdev"] type = "posts" summary = "Fixed navigation bars have an annoying drawback --- they take up precious screen real estate." draft = "" syntax = "1" toc = "" updated = "" +++ {{< image source="/images/adding-headroom-with-javascript.png" title="Internet Browser" >}} Image by janjf93 from Pixabay {{< /image >}} 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 {{< sidenote mark="scroll." set="right" >}} Of course, retracting and expanding navigation bars can be even more annoying. In the [Improvements](#improvements) section we try to ease this pain. {{< /sidenote >}} 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](https://news.ycombinator.com/hn.js) boilerplate to our `app.js` file. ```javascript /** * 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 the navigation bar with the `navbar` selector and grab its height using the `offsetHeight`. ```javascript /** * 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. ```javascript /** * 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. ```javascript 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. ```css .headroom { top: -5em !important; } .navbar { -webkit-transition: top 0.75s; -o-transition: top 0.75s; transition: top 0.75s; } ``` [Demonstration and Source](https://jsbin.com/merekas/edit?js,output) {{< video poster="/images/headroom-normal-scroll.png" source="/videos/headroom-normal-scroll.mp4" options="loop muted" width="360" >}} Basic auto hiding navigation bar. {{< /video >}} ## 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. ```javascript 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. ```javascript 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 {{< sidenote mark="page." set="right" >}}This is an unnecessary check, but the initial auto--hide on page load effect was sort of interesting.{{< /sidenote >}} ```javascript 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](https://jsbin.com/cidaxut/edit?js,output) {{< video poster="/images/headroom-scroll-resistance.png" source="/videos/headroom-scroll-resistance.mp4" options="loop muted" width="360" >}} Improved auto hiding navigation bar. {{< /video >}} ## Conclusion That's it. We can make even {{< sidenote mark="more" set="left" >}} Predict the user's intent by adding a few more basic axioms into the mix. {{< /sidenote >}} improvements but this looks good enough.