Creating a React Nav Header That Slides In and Out of View On Scroll
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 thisHeader
, 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 oful
, 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 (usingflex
).
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 forscroll
, with the callback function beingtoggleScrollDirection
. To prevent memory leaks, we clear the event listener upon theHeader
component unmounting. - We can then define a
const
callscrollDirection
, which calls theuseScrollDirection
calculation. This can then be used to set the conditional class name inheader
.
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>
)
}