How to Toggle Header on Scroll with React.js

This blog post explains how I implemented togglable header in React.js.

Implementation

Let's have a simple Header component. This component assumes that useScrollDetection() function is one of the Effect Hooks and will return Boolean. We toggle the state using CSS, and here different classnames are appended based on the state.

const Header = () => {
  const isHidden = useScrollDirection()
  return (
      <header
        className={
            `header ${isHidden ? "header-hide" : "header-show"}`
        }>
        Hello!
      </header>
  )
}

Here is CSS snippets. Use CSS transitions to achieve smooth hiding/showing effects.

.header {
  position: fixed;
  transition-property: all;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 500ms;
}

.header-show {
  top: 0px;
}

.header-hide {
  top: -6rem;
}

Finally, this is the core logic of the Effect Hook. The key here is to change the state by checking how fast users scroll pages, using diff = 4. You can change this value to tune to your preferences. For example, if you have larger values for this diff, users need to scroll much faster to toggle your header component.

Don't forget to remove event listeners in the cleanup phase, which can be achieve by writing the return function for the effect hook.

function useScrollDirection() {
  const [isHidden, setIsHidden] = useState(false);

  useEffect(() => {
    // store the last scrolled Y to detect how fast users scroll pages
    let lastScrollY = window.pageYOffset

    const updateScrollDirection = () => {
      const scrollY = window.pageYOffset
      const goingDown = scrollY > lastScrollY
      const diff = 4
      // There are two cases that the header might want to change the state:
      // - when scrolling up but the header is hidden
      // - when scrolling down but the header is shown
      // stateNotMatched variable decides when to try changing the state
      const stateNotMatched = goingDown !== isHidden
      const scrollDownTooFast = scrollY - lastScrollY > diff
      const scrollUpTooFast = scrollY - lastScrollY <- diff

      const shouldToggleHeader = stateNotMatched && (scrollDownTooFast || scrollUpTooFast)
      if (shouldToggleHeader) {
        setIsHidden(goingDown)
      }
      lastScrollY = scrollY > 0 ? scrollY : 0
    };

    window.addEventListener("scroll", updateScrollDirection)
    return () => {
      window.removeEventListener("scroll", updateScrollDirection)
    }
  }, [isHidden]);

  return isHidden;
}

Summary

That's it. The core logic is fairly simple, isn't it? Also you can encapsulate the toggle logic into one single function. This is one of the thing I love for React.

October 18, 2022