DEV Community

Guim
Guim

Posted on • Edited on

Hide menu when scrolling in React.js

In this tutorial, I will explain how to make a navbar that is hidden or displayed when we scroll on the page. This is a version for React.js that uses the State of the component to know at all times what is the current state of our navbar.

The component

Now we will see what parts our component needs. First of all, as we said that we will save the position of the scroll in our State, we will create a new value for the State inside the constructor(), which will take the initial value of the offset of the page.

Of course, we will also need the render() method that will return a nav with all the navbar items inside. Here's a first look:

import React, { Component } from "react";
import classnames from "classnames";

export default class Navbar extends Component {
  constructor(props) {
    super(props);

    this.state = {
      prevScrollpos: window.pageYOffset,
      visible: true
    };
  }

  render() {
    return (
      <nav
        className={classnames("navbar", {
          "navbar--hidden": !this.state.visible
        })}
      >
        <a href="#">Item 1</a>
        <a href="#">Item 2</a>
        <a href="#">Item 3</a>
      </nav>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Here's the CSS:

.navbar {
  width: 100%;
  padding: 10px;
  position: fixed;
  top: 0;
  transition: top 0.6s;
}

.navbar--hidden {
  top: -50px;
}
Enter fullscreen mode Exit fullscreen mode

Perfect, our component is ready to be viewed in a browser but does not yet have the behavior we want. Let's go for it!

First, we'll need to do the function that hides or displays the navbar. It will be called as if it was an event. It will see if the current offset is greater or less than the previous offset, depending on whether we scroll up or down. If the offset is bigger, we are going up, therefore it will display the menu. On the contrary, it is going to hide it. This show/hide behavior is managed by the visible state variable.

handleScroll = () => {
  const { prevScrollpos } = this.state;

  const currentScrollPos = window.pageYOffset;
  const visible = prevScrollpos > currentScrollPos;

  this.setState({
    prevScrollpos: currentScrollPos,
    visible
  });
};
Enter fullscreen mode Exit fullscreen mode

Now the function is done. But we need to call it every time the user scrolls on the screen. We will use life cycle methods to add and remove that listener in the scroll.

componentDidMount() {
  window.addEventListener("scroll", this.handleScroll);
}

componentWillUnmount() {
  window.removeEventListener("scroll", this.handleScroll);
}
Enter fullscreen mode Exit fullscreen mode

And with this, we finish our component. Next, I show all the whole code. I hope you liked it, I'll be uploading content more often. See you soon!

import React, { Component } from "react";
import classnames from "classnames";

export default class Navbar extends Component {
  constructor(props) {
    super(props);

    this.state = {
      prevScrollpos: window.pageYOffset,
      visible: true
    };
  }

  // Adds an event listener when the component is mount.
  componentDidMount() {
    window.addEventListener("scroll", this.handleScroll);
  }

  // Remove the event listener when the component is unmount.
  componentWillUnmount() {
    window.removeEventListener("scroll", this.handleScroll);
  }

  // Hide or show the menu.
  handleScroll = () => {
    const { prevScrollpos } = this.state;

    const currentScrollPos = window.pageYOffset;
    const visible = prevScrollpos > currentScrollPos;

    this.setState({
      prevScrollpos: currentScrollPos,
      visible
    });
  };

  render() {
    return (
      <nav
        className={classnames("navbar", {
          "navbar--hidden": !this.state.visible
        })}
      >
        <a href="#">Item 1</a>
        <a href="#">Item 2</a>
        <a href="#">Item 3</a>
      </nav>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (15)

Collapse
 
arobert93 profile image
Alexandru Robert

Hi there! Isn't it easier and a lot more "React" to use the top position as a state key instead of having to query by element on each scroll event? If we talk about performance, document.getElement... is not the best option. And if we want to reuse the same component, that ID will be a problem later. Hope it helps. Kudos for the article!

Collapse
 
gnuns profile image
Gabriel Nunes

Storing the top as a state key is probably better, but to use React.createRef() is also an alternative to getElementById.

Collapse
 
guimg profile image
Guim

Thank you both! I've made some changes on the nav id and how I manage the CSS too.

Collapse
 
guimg profile image
Guim

About the top as a state key, If you could provide me with some way to test the efficiency I'll be thankful. I'll try it by myself also 😊

Collapse
 
arobert93 profile image
Alexandru Robert

Hello Guim,

In Chrome Developer Tools under "Performance" tab you can record a new session. This will display your frame rate in idle state and animation/scroll action. But it will be really hard to see any difference with a fresh project. It will become obvious when you have a long list of DOM nodes, then document.getElement on each ~10ms event spawn (the duration between each scroll event execution, when the scrollHandler is executed) really effects the user experience and sometimes with some animations on top, the page becomes unusable.

About your code update, you can remove the "ref" since it is not used. And as a note that can help you some time in future, "refs" can't be used in componentDidMount/componentWillMount or any function triggered before rendering in the component lifecycle.

Also, please update your CSS. You should have two different classes. One that keeps your base component aspect, and one that will be used depending on component state to update your base style.


.navbar {
    width: 100%;
    padding: 10px;
    position: fixed;
    top: 0;
    left: 0;
    transition: top 0.6s;
}

.navbar--hidden {
    top: -50px;
}

Have a nice day!

Thread Thread
 
guimg profile image
Guim

Great contribution! Thanks. I updated the CSS and removed the ref too.

Thread Thread
 
arobert93 profile image
Alexandru Robert

Perfect! As a final step, you could also change a bit the setState. Instead of having to write 3 times setState, you could do:

handleScroll = () => {
  const { prevScrollpos } = this.state;

  const currentScrollPos = window.pageYOffset;
  const visible = prevScrollpos > currentScrollPos;

  this.setState({
    prevScrollpos: currentScrollPos,
    visible
  });
};

It would be better to check if scroll direction (-Y or Y+) is the same with previous scroll direction. If it is the same, you don't have to update the state, because the component has the same position. By this I mean, if you already have the component hidden, and you keep scrolling down, you don't need to update its state.

And to be fair, I would add a debounce function wrapper to avoid calling this handler too many times (for performance reasons). Look into debounce method of "underscore" node modules. It's pretty easy to implement, and saves a lot of memory.

As always, simple problems have complex solutions. :)

Thread Thread
 
guimg profile image
Guim

Done! Thanks for your good advice 🌟

Collapse
 
jafaircl_66 profile image
jafaircl

Awesome write up! I just wrote about doing this same thing using hooks and RxJS if you’re interested in taking a look at a different approach: faircloth.xyz/auto-hiding-sticky-h...

Collapse
 
guimg profile image
Guim

I'll take a look! :)

Collapse
 
nickcrews profile image
Nick Crews

This is great! One tiny improvement, to get rid of the classnames dependency, you could use string formatting, e.g.

className={`navbar${this.state.visible ? "" : " navbar--hidden" }`}
Enter fullscreen mode Exit fullscreen mode

Related but separate question: I have this implemented and it's working great, but the top of the main content is chopped off by the navbar when scrolled to the top of the page. The main content needs to be bumped down by the height of the navbar. Is there a good way to do this?

Collapse
 
scottdet profile image
scottdet

This works great for me on desktop, however on mobile it's a bit jumpy - does anyone have a solution for this? I was reading this blog: remysharp.com/2012/05/24/issues-wi... which suggests an issue with using 'position: fixed;' inside the scroll element, but the solutions there don't seem to work either.

On mobile, if I scroll too quickly to the top or bottom of the page it seems that pageYOffset picks up some bad values and makes my menu disappear even if I'm scrolling back to the top of the page.

Collapse
 
abodmicheal profile image
Abod Micheal (he/him)

bro let's make this a package , I also gave a tutorial on something like this

Collapse
 
yousefmms profile image
Yousef Mahmoud • Edited

Hi,

Nice solution!

Do you have how it's possible to unit test this component - test the window dependency?

Collapse
 
samx23 profile image
Sami Kalammallah

Thanks