Subscribe to get the weekly post

Or

Follow me

Build a sticky navigation bar with React

Published 2 months ago 6 min readTraduire en francais

I'm pretty sure, you've already seen the effect we're going to make today. It's a common animation that we see on a lot of websites. When the user scrolls, the navigation bar moves down with a cool animation effect.

So you're lucky because, in this post, we'll do the same effect with React by building a sticky navbar from scratch without third party libraries.

You can check it live here

Setting up the project

To be able to follow along, you'll need to create a new React app with :

npx create-react-app react-sticky-navbar

Or, if you want, you can set up a new one from scratch with Webpack. It's really up to you.

Then, we'll need to create a couple of files.

Notice that I'm going to focus mostly on the navbar files, to make this post short and useful, you can still find the source code at the end of the article.

  • Create a components folder and as sub-folder Header. Then, add Welcome.js, Welcome.css, Navbar.js and Navbar.css.
  • In Welcome.js
import React from "react"
import "./Welcome.css"
import Logo from "../../assets/images/logo.svg"
import About from "./About"

const Welcome = ({ stickyRef }) => (
  <main>
    <section className="welcome">
      <div ref={stickyRef}>
        <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 stickyRef. It's just the reference of the element that will fire the sticky effect later on the scroll. By the way, here I use destructuring to pull out the element. If you want, you can use props.stickyRef. It's up to you

Now, let's move on to the next file.

  • In 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

In this file, for the moment, it's very simple. So, later we're going to update it though, to be able to render some elements conditionally and make the navigation bar sticky.

gif-1

The sticky effect

  • In App.js
import React, { Fragment, useEffect, useRef, useState } from "react"
import Welcome from "./components/Header/Welcome"
import Navbar from "./components/Header/Navbar"

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

  const [isSticky, setSticky] = useState(false)

  const stickyRef = useRef(null)
  const handleScroll = () => {
    window.pageYOffset > stickyRef.current.getBoundingClientRect().bottom
      ? setSticky(true)
      : setSticky(false)
  }

  window.addEventListener("scroll", handleScroll)

  return (
    <Fragment>
      <Navbar sticky={isSticky} />
      <Welcome stickyRef={stickyRef} />
    </Fragment>
  )
}

export default App

All the magic will happen here (i promise). We first need to import a couple of hooks and Fragment which wraps our elements.

Then, we define our state with useState() and set it to false. That's mean, now we'll be able to switch isSticky between true and false depending on the scroll.

gif-2

When the user starts scrolling, the handleScroll() will be fired. Then, it checks if the window.pageYOffset > stickyRef.current.getBoundingClientRect().bottom and handles isSticky. In other words, it will verify if the number of pixels the page is currently scrolled along the vertical axis is superior or not to the position of the current element relative to its bottom.

Then, we pass to the Navbar component the isSticky state and Welcome the reference of the element handled by useRef. Now, we need to update a little bit Navbar.js to make it sticky on the scroll.

  • In 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

We've now access to sticky, and we can check if it's true or false and render classes or elements conditionally with the ternary operator.

We do a lot, but we've not done yet. It remains an important part: styling and animations.

Let's do it!

gif-3

Style 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, besides fixing the navigation bar on scroll with the .navbar-sticky class, we make a nice moveDown effect with @keyframes. Then, we'll also rotate the logo a little bit to make everything look good and smooth on the scroll.

So, we finally get our sticky navigation bar. However, add scrolling listeners like that can cause performance issues on your app. You can verify it by logging the scroll event on your console.

However, we can still do something to handle the issue. We can use a debounce function.

Now, we need to update a little bit App.js to handle that issue.

Debounce it

  • In App.js
import React, { Fragment, useEffect, useRef, useState } from "react"
import Welcome from "./components/Header/Welcome"
import Navbar from "./components/Header/Navbar"

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

  const [isSticky, setSticky] = useState(false)

  const stickyRef = useRef(null)
  const handleScroll = () => {
    window.pageYOffset > stickyRef.current.getBoundingClientRect().bottom
      ? setSticky(true)
      : setSticky(false)
  }

  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)
    }
  }

  window.addEventListener("scroll", debounce(handleScroll))

  return (
    <Fragment>
      <Navbar sticky={isSticky} />
      <Welcome stickyRef={stickyRef} />
    </Fragment>
  )
}

export default App

You can find a debounce method all over the place, or you can even use lodash. But, the most important thing to understand is what this function does. Instead of running handleScroll all the time, it will run every 20 milliseconds to give you more control.

With that being said, we've finished building our sticky navbar.

Source code

applause

That's all folks!

Thanks for reading it.

#react

Posted by Ibrahima Ndaw

Full-stack developer and blogger
Follow me

Get the next in your inbox