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.
Sorry for the interrupt!
If you're interested in learning React in a comprehensive way, I highly recommend this bestseller course: React - The Complete Guide (incl Hooks, React Router, Redux)
It's an affiliate link, so by purchasing, you support the blog at the same time.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