Build a sticky navigation bar with React

Subscribe to receive the free weekly article

This article has been updated on 2020-10-27

In this tutorial, we are going to build a sticky navigation bar using React hooks.

You can preview the finished project in this CodeSandbox.

Setting up the project

To be able to follow along, you need to create a new React app, so run the following command in your command-line interface (CLI):

npx create-react-app react-sticky-navbar

Next, structure the project as follows.

src
├── App.js
├── assets
|  └── images
|     └── logo.svg
├── components
|  └── Header
|     ├── About.js
|     ├── Navbar.css
|     ├── Navbar.js
|     ├── Welcome.css
|     └── Welcome.js
├── hooks
|  └── useSticky.js
├── index.css
├── index.js
├── serviceWorker.js
└── setupTests.js

I will mostly focus on the navbar files to make this article short and useful. You can still check the source code for a better understanding. So, let's get hands dirty by writing some meaningful code.

  • Header/Welcome.js
import React from "react"

import "./Welcome.css"
import Logo from "../../assets/images/logo.svg"
import About from "./About"

const Welcome = ({ element }) => {
  return (
    <main>
      <section className="welcome">
        <div ref={element}>
          <img src={Logo} alt="logo" className="welcome--logo" />
          <p>Even if you scroll, I will stick with you</p>
          <button className="welcome__cta-primary">Contact us</button>
        </div>
      </section>
      <About />
    </main>
  )
}

export default Welcome

As you can see, here, we have a simple component that receives the props element as a parameter. It is a reference to the element that will fire the sticky effect when scrolling.

By the way, I used destructuring to pull out the element from the props object. You can alternatively use props.element.

Now, let's move to the next file and create the navigation bar skeleton.

  • Header/Navbar.js
import React from "react"
import "./Navbar.css"
import Logo from "../../assets/images/logo.svg"

const Navbar = () => (
  <nav className="navbar">
    <div className="navbar--logo-holder">
      <img src={Logo} alt="logo" className="navbar--logo" />
      <h1> Stick'Me</h1>
    </div>
    <ul className="navbar--link">
      <li className="navbar--link-item">Home</li>
      <li className="navbar--link-item">About</li>
      <li className="navbar--link-item">Blog</li>
    </ul>
  </nav>
)
export default Navbar

For now, we have a simple component. We will update it later to display the elements conditionally and make the navigation bar sticky.

The sticky effect

For the sticky effect, we will create a custom hook to handle it and then use it in our components.

  • hooks/useSticky.js
import { useEffect, useState, useRef } from "react"

function useSticky() {
  const [isSticky, setSticky] = useState(false)
  const element = useRef(null)

  const handleScroll = () => {
    window.scrollY > element.current.getBoundingClientRect().bottom
      ? setSticky(true)
      : setSticky(false)
  }

  // This function handles the scroll performance issue
  const debounce = (func, wait = 20, immediate = true) => {
    let timeOut
    return () => {
      let context = this,
        args = arguments
      const later = () => {
        timeOut = null
        if (!immediate) func.apply(context, args)
      }
      const callNow = immediate && !timeOut
      clearTimeout(timeOut)
      timeOut = setTimeout(later, wait)
      if (callNow) func.apply(context, args)
    }
  }

  useEffect(() => {
    window.addEventListener("scroll", debounce(handleScroll))
    return () => {
      window.removeEventListener("scroll", () => handleScroll)
    }
  }, [debounce, handleScroll])

  return { isSticky, element }
}

export default useSticky

All the magic will happen here! I promise.

We first need to import some hooks from React and then define our state with the useState() hook. That means, now, we'll be able to switch between true and false depending on the scroll.

When the user scrolls, the function handleScroll() will be called. It checks whether window.scrollY is superior or not to stickyRef.current.getBoundingClientRect().bottom and then handles the isSticky state consequently.

This function will check if the number of pixels the page has currently scrolled along the vertical axis is superior or not to the position of the current element relative to its bottom.

Next, we use a debounce function to throttle the scrolling event to avoid performance issues. Instead of running handleScroll all the time, it will run every 20 milliseconds to give you more control.

With this in place, we can now listen to the scroll event when the component is mounted and remove listeners when unmounted.

Great! Now, to make the custom hook usable in other files, we have to return something from it. So, we need to return the isSticky state and the element reference as values.

  • Header/Navbar.js
import React from "react"
import "./Navbar.css"
import Logo from "../../assets/images/logo.svg"

const Navbar = ({ sticky }) => (
  <nav className={sticky ? "navbar navbar-sticky" : "navbar"}>
    <div className="navbar--logo-holder">
      {sticky ? <img src={Logo} alt="logo" className="navbar--logo" /> : null}
      <h1> Stick'Me</h1>
    </div>
    <ul className="navbar--link">
      <li className="navbar--link-item">Home</li>
      <li className="navbar--link-item">About</li>
      <li className="navbar--link-item">Blog</li>
    </ul>
  </nav>
)
export default Navbar

This component receives the sticky state as props. Next, we check if it's true or false and show classes or elements conditionally with the help of the ternary operator.

So far, we have covered a lot. However, we miss an important part: styling and animations. Let's do that in the next section.

Styling the navbar

  • In Navbar.css
.navbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 2.5rem;
  position: absolute;
  z-index: 1;
  width: 100%;
}

.navbar-sticky {
  background: #333;
  position: fixed;
  top: 0;
  left: 0;
  box-shadow: 1px 1px 1px #222;
  animation: moveDown 0.5s ease-in-out;
}

.navbar--logo {
  width: 2rem;
  height: 2rem;
  margin-right: 0.5rem;
  animation: rotate 0.7s ease-in-out 0.5s;
}

@keyframes moveDown {
  from {
    transform: translateY(-5rem);
  }
  to {
    transform: translateY(0rem);
  }
}

@keyframes rotate {
  0% {
    transform: rotateY(360deg);
  }
  100% {
    transform: rotateY(0rem);
  }
}

Here, we fix the navigation bar on scroll using the .navbar-sticky class. Next, we use moveDown to make the animation effect that rotates the logo a bit to make it smooth on the scrolling.

With this step forward, we can now use the App.js file to display our components when the page loads.

  • App.js
import React from "react"
import useSticky from "./hooks/useSticky.js"
import Welcome from "./components/Header/Welcome"
import Navbar from "./components/Header/Navbar"

function App() {
  const { isSticky, element } = useSticky()
  return (
    <>
      <Navbar sticky={isSticky} />
      <Welcome element={element} />
    </>
  )
}

export default App

As you can see, here, we import the components and the custom hook. With this, we can now pass down the props and handle the sticky effect appropriately.

That's it! We are now done building a sticky navbar using React hooks.

Thanks for reading it.

You can find the Source code here