Hide/Show header on scroll down/up using pure javascript

frontend6 Min to Read01 Aug 17

You might have seen one cool UI feature in many modern websites that when you scroll down the page, header hides and shows-up when you scroll up. It gives much more room to present the primary content on the screen. This becomes much more critical if the screen size is constraint like mobile devices. So this feature is very optimal and user-friendly. It gives you the ability to have your user’s full attention on primary content and show up secondary content like header, only when needed.

In this Article, we will implement this feature using pure Javascript code. By word ‘pure’, I mean without any library. Generally, Well tested library code will be more performant, have wide browser support and less buggy but may have big file size. Sometimes, We just want to implement it ourselves because we don’t want to include huge library as a dependency, or may be because it does not fit into your existing workflow or for any other reason i.e you want to learn by implementing it by yourself.

We will also write small HTML and CSS to complete the demo. First, we will try to implement this with basic unoptimized Javascript code. Later on, we will see if any optimization is needed and why.

Basic Code

body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}
.header {
  background-color: teal;
  color: #ffffff;
  position: fixed;
  width: 100%;
  height: 65px;
}
.main {
  background-color: rgba(0, 0, 0, 0.1);
  height: 150vh;
  padding-top: 70px;
}
.header-unpin {
  display: none;
}
.header-pin {
  display: block;
}
var lastKnownScrollY = 0
var currentScrollY = 0
var ticking = false
var idOfHeader = "header"
var eleHeader = null
const classes = {
  pinned: "header-pin",
  unpinned: "header-unpin",
}
function onScroll() {
  currentScrollY = window.pageYOffset
  if (currentScrollY < lastKnownScrollY) {
    pin()
  } else if (currentScrollY > lastKnownScrollY) {
    unpin()
  }
  lastKnownScrollY = currentScrollY
}
function pin() {
  if (eleHeader.classList.contains(classes.unpinned)) {
    eleHeader.classList.remove(classes.unpinned)
    eleHeader.classList.add(classes.pinned)
  }
}
function unpin() {
  if (
    eleHeader.classList.contains(classes.pinned) ||
    !eleHeader.classList.contains(classes.unpinned)
  ) {
    eleHeader.classList.remove(classes.pinned)
    eleHeader.classList.add(classes.unpinned)
  }
}
window.onload = function() {
  eleHeader = document.getElementById(idOfHeader)
  document.addEventListener("scroll", onScroll, false)
}
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>
  <body>
    <header id="header" class="header">
      Header
    </header>
    <main class="main">
      <p>Lorem Ipsum is simply dummy....</p>
      <p>Lorem Ipsum is simply dummy....</p>
      <p>Lorem Ipsum is simply dummy....</p>
    </main>
  </body>
</html>

The basic logic is: listen for scroll event, get the scroll-y position and determine if its scroll-up or scroll-down. After determination, apply the appropriate CSS class to the header element.

In above code: in CSS, we have .header-pin and .header-unpin classes for hiding and showing header respectively. To note, header must have position: fixed style, others are cosmetic changes. In Javascript, We are calling onScroll() function on each scroll event. Inside it, we are getting currentScrollY and then comparing it with lastKnownScrollY.

If it is greater, it means, its scroll-down, hence we are calling pin() function otherwise unpin(). pin function is removing unpinned class and adding pinned class, only if its already unpinned. unpin function has a simillar but opposite check. This code is enough to auto hide header on scroll, but not optimized for a modern browser. See below points on where and why we need to optimize.

60 FPS

In order to maintain 60 FPS, Browser gets 16 milliseconds to complete one frame. If it takes more than that, then the frame is dropped resulting in judder screen. For example, Suppose our code to make visual changes take 8 ms and started running at 9th millisecond from the start of frame, then it will result in the missing frame.

So the best time to make visual changes is at the start of frame and finish it as soon as possible. In above code, there is no guarantee that pin() and unpin() code, responsible for making visual changes in the header, will run at the start of the frame. To make it run at the start of frame, we will use requestAnimationFrame() api in our next optimized code.

expensive style change

Whenever you make any changes to CSS style or attribute of HTML element, the browser generally perform three steps. 1. Layout calculation to decide where to place the element on the screen. 2. Paint operation to start filling actual pixel on the screen. 3. Compositing multiple layers if any. Each of this operation can be quite expensive depending on actual use case, resulting in page low performance. Now we will see, which CSS properties triggers which operation.

There are some CSS properties i.e display that require all three operations, others i.e background-color require only paint and composite operation, while there are two properties i.e opacity and transform, as of now, that require only compositing. So it makes sense to stick to compositing properties only for more performance gain. Hence, In above code, we will replace display to transform, in our next optimized code.

Optimazation

.header {
  transition: transform 0.25s ease-in-out;
}
.header-unpin {
  display: none;
  transform: translateY(-65px);
}
.header-pin {
  display: block;
  transform: translateY(0);
}
var lastKnownScrollY = 0
var currentScrollY = 0
var ticking = false
var idOfHeader = "header"
var eleHeader = null
const classes = {
  pinned: "header-pin",
  unpinned: "header-unpin",
}
function onScroll() {
  currentScrollY = window.pageYOffset
  requestTick()
}
function requestTick() {
  if (!ticking) {
    requestAnimationFrame(update)
  }
  ticking = true
}
function update() {
  if (currentScrollY < lastKnownScrollY) {
    pin()
  } else if (currentScrollY > lastKnownScrollY) {
    unpin()
  }
  lastKnownScrollY = currentScrollY
  ticking = false
}
function pin() {
  if (eleHeader.classList.contains(classes.unpinned)) {
    eleHeader.classList.remove(classes.unpinned)
    eleHeader.classList.add(classes.pinned)
  }
}
function unpin() {
  if (
    eleHeader.classList.contains(classes.pinned) ||
    !eleHeader.classList.contains(classes.unpinned)
  ) {
    eleHeader.classList.remove(classes.pinned)
    eleHeader.classList.add(classes.unpinned)
  }
}
window.onload = function() {
  eleHeader = document.getElementById(idOfHeader)
  document.addEventListener("scroll", onScroll, false)
}

As discussed, we have modified our CSS and Javascript code in above code snippet. CSS changes are pretty basic. In Javascript, we have introduce two new function requestTick() and update(). Now, whenever scroll event occurs, We requestAnimationFrame to run our update function that does the main stuff of pinning and unpinning header. Another important thing to note here is that we are calling requestAnimationFrame only if previous one is finished using ticking flag, this is called debouncing.

So for optimization: always use requestAnimationFrame api for visual changes. Debounce the scroll event handler. Stick to compositer properites for style changes as much as possibe.

Reference

If you loved this post, Please share it on social media.