DEV Community

Cover image for Show and hide a header based on scroll direction
Chris Bongers
Chris Bongers

Posted on • Originally published at daily-dev-tips.com

Show and hide a header based on scroll direction

This article actually has a funny origin as it was requested by my good friend Fredrik asked me to help with a specific menu.

He initially reached out to me, thanking me for writing down the article on showing a menu on scroll.

And he wanted to create something similar to the Pentagram website.

Let's take a moment to see what happens and what kind of actions we need to focus on.

  1. We see the header with no background sitting over an image
  2. On scroll, the header disappears like a regular element
  3. Once we scroll down and pass the first viewport height, the following actions can happen
  4. Scroll up, the menu re-appears with a background
  5. Scroll down, the menu disappears again
  6. When we hit the viewport height, it always disappears again

I've done some more research on this website, and they actually use two headers to achieve this effect. However, I'm going to show you how to do this with just one!

The result for today can be seen in this CodePen.

Sketching a solution

Let's start by wireframing an HTML setup to work with. I went for a straightforward approach and came up with the following.

<header>Logo</header>
<main>
  <section><img src="img.jpg" alt="colorfull passage" /></section>
  <section><p>text</p></section>
  <section><img src="img.jpg" alt="colored leafs" /></section>
  <section><p>text</p></section>
</main>
Enter fullscreen mode Exit fullscreen mode

As you can see, we have the header as one element and a main wrapper with some sections.

I'll start by making each section the exact size of the viewport. This will make the effect stand out a bit more.

section {
  background: #efefef;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}
Enter fullscreen mode Exit fullscreen mode

Note: You can find the complete CSS in the CodePen example.

Then we have to start working on the initial header styling.
As mentioned, it should be an absolute positioned element, so it will scroll away initially.

header {
  position: absolute;
  width: 100%;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.4);
}
Enter fullscreen mode Exit fullscreen mode

Handling scroll events in JavaScript

Now I think it's time to start adding some scroll listeners in JavaScript.

As you might know, listening to scroll events has a high impact on performance because it fires too often.
Especially on mobile devices, it fires like crazy.

So, we want to add some kind of threshold to not fire too many events.

I've decided on a 100ms delay of firing. You can play around with this value. However, it will impact when it adds/removes certain classes to get weird behaviors.

The throttle function looks like this:

const throttle = (func, time = 100) => {
  let lastTime = 0;
  return () => {
    const now = new Date();
    if (now - lastTime >= time) {
      func();
      time = now;
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

Basically, this will check if enough time is passed. If that is the case, we fire the func() we passed as an argument.

To use this we can wrap the function we want to use for the scroll effect like so:

window.addEventListener('scroll', throttle(validateHeader, 100));
Enter fullscreen mode Exit fullscreen mode

So on scroll, but only after 100ms will we fire a validateHeader function.

Before building this function, let's set up some variables we need.
We want to have the header element and the last scrolled position in this case.

const header = document.querySelector('header');
let lastScroll = 0;
Enter fullscreen mode Exit fullscreen mode

Now it's time to make the validateHeader function.

const validateHeader = () => {
  // todo
};
Enter fullscreen mode Exit fullscreen mode

We can start by getting the current scroll offset and the screen size.

const windowY = window.scrollY;
const windowH = window.innerHeight;
Enter fullscreen mode Exit fullscreen mode

The first check we need to do is determine if we scrolled past the first viewport height (windowH).

if (windowY > windowH) {
  // We passed the first section, set a toggable class
  header.classList.add('is-fixed');
} else {
  header.classList.remove('is-fixed', 'can-animate');
}
Enter fullscreen mode Exit fullscreen mode

We will add a new class to our header if this is the case. This class is the is-fixed class.

If the scroll is not high enough, we remove this class and the can-animate class we'll add in a second.

This is-fixed class looks like this:

header {
  &.is-fixed {
    background: rgba(255, 255, 255, 0.9);
    position: fixed;
    transform: translate3d(0, -100%, 0);
  }
}
Enter fullscreen mode Exit fullscreen mode

This class changes the header from absolute to fixed and makes sure it's hidden initially. It also changes the background of the header.

The next thing we need is to determine if we passed the viewport height + the size of the header.
I split these two to prevent flickering from happening because of the animation we will set.

// Determine is we ready to animate
if (windowY > windowH + 40) {
  header.classList.add('can-animate');
} else {
  header.classList.remove('scroll-up');
}
Enter fullscreen mode Exit fullscreen mode

This can-animate class will add the smooth animation we want. However, as mentioned, we don't want to on the first load. That's why we split the two.

header {
  &.can-animate {
    transition: transform 0.3s ease, visibility 0s 0.3s linear;
  }
}
Enter fullscreen mode Exit fullscreen mode

The last part of this puzzle is the actual show once we scroll upwards.

if (windowY < lastScroll) {
  header.classList.add('scroll-up');
} else {
  header.classList.remove('scroll-up');
}
Enter fullscreen mode Exit fullscreen mode

You can see we evaluate if the window position is smaller than the last scrolled position.
If yes, it means we should scroll up and add the scroll-up class.

This class will transform the negative position of the header.

header {
  &.scroll-up {
    transform: translate3d(0, 0, 0);
  }
}
Enter fullscreen mode Exit fullscreen mode

The last thing this function needs is to update the last scroll position with the current one.

lastScroll = windowY;
Enter fullscreen mode Exit fullscreen mode

And that's it, we got ourselves a header that can change appearance once it passes the first viewport height.
And it will show only on scroll up.

Note: You can see the complete code in the embedded CodePen.

I hope you enjoyed this article. I would love to see what you used this for.

Thank you for reading, and let's connect!

Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter

Discussion (6)

Collapse
lexlohr profile image
Alex Lohr

One tiny optimization: classList.toggle has a little known optional second boolean argument which overrides the class being (in)active. So

if (windowY < lastScroll) {
  header.classList.add('scroll-up');
} else {
  header.classList.remove('scroll-up');
}
Enter fullscreen mode Exit fullscreen mode

becomes

header.classList.toggle('scroll-up', windowY < lastScroll);
Enter fullscreen mode Exit fullscreen mode
Collapse
dailydevtips1 profile image
Chris Bongers Author

Oh nice actually never used that one, time to look into that
Thanks Alex 💖

Collapse
joelbonetr profile image
JoelBonetR

Just a thing, I'm on my smartphone (Google Pixel 5 just for screen reference) and it does not work on the first "half" which is weird specialy on mobile which is the desirable target place for this feature.

That being said I'll totally change the condition for that. If you think on a landscape it will work -probably- but on portrait, Y is always greater than X axis.

Collapse
dailydevtips1 profile image
Chris Bongers Author

Hey Joel,

It should not work on the first section on all devices, is that what you mean?
The rest of the condition should fire, so from the second onwards you should see the header when scrolling up.

But i'll double check my mobile implementation.

Collapse
joelbonetr profile image
JoelBonetR

I mean that this feature is specially useful in mobile but you decided to make it to not work in the first section, which makes it unusable for some piece of screen, I've just wondering why

Thread Thread
dailydevtips1 profile image
Chris Bongers Author

Ah right!
It's mainly because the page the idea comes from uses that approach so wanted to make sure to include that as well.