Avatar

Nǐ hǎo, I'm Julia.

Creating a React Nav Header That Slides In and Out of View On Scroll

#css #react

4 min read

Here a quick post on how to create a navigation header for a React / NextJS app. Instead of having a fixed nav bar that always appears at the top of my app, I wanted it to slide up and out of view as a user scrolls down the page. I also wanted it to reappear by sliding down and into view as a user scrolls up the page.

Set up the base component with styling

The base component looks like this:

import Link from 'next/link'
import React, { useEffect, useState } from 'react'

export const Header = () => {
  return (
    <header>
      <div className="container">
        <nav role="navigation">
          <menu>
            <li>
              <Link href="/link-1">
                <a>Link 1</a>
              </Link>
            </li>
            <li>
              <Link href="/link-2">
                <a>Link 2</a>
              </Link>
            </li>
            <li>
              <Link href="/link-3">
                <a>Link 3</a>
              </Link>
            </li>
          </menu>
        </nav>
      </div>
    </header>
  )
}

You can style it however you want, but let’s say something simple like what I have below. The main things to note here are:

  • On component load, our Header component will be located at the top of the screen. Depending on the parent component that holds this Header, you might need to set a width on <header>.
  • The default background colour will be transparent, so that it matches the background colour of whatever the underlying screen content is.
  • menu is a more semantic tag we can use instead of ul, but effectively behaves in the same way. This is where I declare that I don’t want child list items to have a marker. I also want the 3 links to appear in a straight line from left to right on the screen (using flex).
header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  background-color: transparent;
  z-index: 900;
}

.container {
  height: 72px;
}

nav {
  height: 100%;

  menu {
    margin: 0;
    padding: 0;
    display: flex;
    list-style-type: none;

    li {
      a {
        padding: 24px 16px;
        display: block;
        color: blue;
        font-size: 16px;
        line-height: 24px;
        transition: color 0.3s ease-in;

        &:hover {
          color: black;
        }
      }
    }
  }
}

Define the conditional classes

Next up, we want to define some classes for the behaviour we want the Header to take, depending on whether the user is scrolling down or up. Note here, the addition of .scroll-down and .scroll-up within header. What we’re saying is that if header has a class called scroll-down applied to it, move it vertically up by 100% of its height. To give it a nice slide effect, we set the transition on transform in header's CSS (I’ve also added a subtle background colour transition here). We do the opposite for the scroll-up class, where we’re saying header should be at its default position.

header {
  transition: transform 0.3s ease-in, background-color 0.3s linear;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  background-color: transparent;
  z-index: 900;

  &.scroll-down {
    transform: translateY(-100%);
    background-color: white;
  }

  &.scroll-up {
    transform: translateY(0);
    background-color: white;
  }
}

Set up listeners to determine scroll direction

Check out the useScrollDirection function I added below. How it works is as follows:

  • We track the state of 2 variables (using useState) - the current scroll direction, and the previous Y-axis offset.
  • The toggleScrollDirection function then checks the current Y-axis position a user is at and compares it to the previous offset. If it is greater than the previous offset (and the current position is more than 50px below the top of the page), we know the scroll direction is down, otherwise, it’s up.
    • The reason for adding the 50px check is just because I wanted to show the header for a period even when the user starts scrolling, rather than immediately having it slide out of view.
    • As a last step, we set the prevOffset state to be the current position.
  • The useEffect then adds an event listener for scroll, with the callback function being toggleScrollDirection. To prevent memory leaks, we clear the event listener upon the Header component unmounting.
  • We can then define a const call scrollDirection, which calls the useScrollDirection calculation. This can then be used to set the conditional class name in header.
import Link from 'next/link'
import React, { useEffect, useState } from 'react'

export const Header = () => {
  const useScrollDirection = () => {
    const [scrollDirection, setScrollDirection] = useState('')
    const [prevOffset, setPrevOffset] = useState(0)

    const toggleScrollDirection = () => {
      let scrollY = window.scrollY
      if (scrollY > prevOffset && scrollY > 50) {
        setScrollDirection('down')
      } else if (scrollY < prevOffset && scrollY > 50) {
        setScrollDirection('up')
      } else {
        setScrollDirection('')
      }
      setPrevOffset(scrollY)
    }

    useEffect(() => {
      window.addEventListener('scroll', toggleScrollDirection)
      return () => {
        window.removeEventListener('scroll', toggleScrollDirection)
      }
    })
    return scrollDirection
  }

  const scrollDirection = useScrollDirection()

  return (
    <header className={`scroll-${scrollDirection}`}>
      <div className="container">{/* ... same as before */}</div>
    </header>
  )
}

© 2016-2024 Julia Tan · Powered by Next JS.