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.
- We see the header with no background sitting over an image
- On scroll, the header disappears like a regular element
- Once we scroll down and pass the first viewport height, the following actions can happen
- Scroll up, the menu re-appears with a background
- Scroll down, the menu disappears again
- 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>
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;
}
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);
}
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;
}
};
};
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));
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;
Now it's time to make the validateHeader
function.
const validateHeader = () => {
// todo
};
We can start by getting the current scroll offset and the screen size.
const windowY = window.scrollY;
const windowH = window.innerHeight;
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');
}
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);
}
}
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');
}
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;
}
}
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');
}
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);
}
}
The last thing this function needs is to update the last scroll position with the current one.
lastScroll = windowY;
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