How to make nav bar styles in React change on scroll

Learn how to change the styles of a navigation bar when a user begins to scroll and improve your user experience with a custom React hook and Tailwind CSS.

Hooking up your navigation bar to respond to user interaction is a great way to improve the user experience. For example, when the user scrolls down, you can add a slight shadow to the top of the navigation to make it look like it's lying on the page.

In this lesson, you'll use a custom React hook and Tailwind CSS to change the styles of a navigation bar when a user begins to scroll.

Watch the lesson

Project repo

The code for this project is open-source and available on GitHub. I use Gumroad for those that want to be generous and donate, but no purchase is required. 😊

Grab the code.

Next.js and Tailwind CSS starter

I chose to use Tailwind for styling my Next.js application. You can read more in the Tailwind installation guide if you're interested in how to implement Tailwind into Next.js or another React framework like Create React App or Gatsby.

I'm also using pre-built React components from Tailwind UI. They have an entire library that you can purchase, but I'm just using the free Tailwind UI preview components in this lesson.

Make the Nav sticky

The code for the Nav component can be found on Tailwind UI. The Nav isn't sticky by default, but making it sticky is simple. All you need to do to make the Nav sticky is to add the sticky and top-0 Tailwind CSS classes to the Popover component.

export const Nav = () => {
return <Popover className="sticky top-0 bg-white">...</Popover>
}

The sticky class will set the CSS position property to sticky, and the top-0 will set the top placement to 0px, which means the Nav will stick to the top of the page.

Next, add the Nav to the index.js file and run npm run dev in your terminal to get your development server started. Your index.js file should look like this:

import { Nav } from 'components'
export default function Home() {
return (
<>
<Nav />
</>
)
}

You should now see the Nav at localhost:3000, but the page will not be scrollable because there's not enough content. To fix this, you can add components to the page to make it higher, as I did in the final project. However, a simple fix is to create a div and set the height to a big value, which will force the page to be scrollable:

import { Nav } from 'components'
export default function Home() {
return (
<>
<Nav />
<div className="h-[3000px]"></div>
</>
)
}

Now that the page is scrollable, it's time to create the custom React hook to track the user's scroll position.

Create the useScrollPosition hook

Create a new folder in your project called hooks and add a 'useScrollPosition' file. Inside the file, add the following code:

import { useEffect, useState } from 'react'
export const useScrollPosition = () => {
const [scrollPosition, setScrollPosition] = useState(0)
useEffect(() => {
const updatePosition = () => {
setScrollPosition(window.pageYOffset)
}
window.addEventListener('scroll', updatePosition)
updatePosition()
return () => window.removeEventListener('scroll', updatePosition)
}, [])
return scrollPosition
}

This custom hook uses a React useState hook to track a value named scrollPosition and a function called setScrollPosition to update the state value.

The hook uses the React useEffect hook to create an event listener when the component first mounts. This event listener is on the scroll event and will call the updatePosition function any time this scroll event is triggered.

The useEffect hook also calls the function inside, which accounts for the scroll position when the component first mounts. Last, the useEffect hook has a cleanup function that removes the event listener when the component unmounts.

The custom hook returns the scrollPosition value to get access to the values we need from the hook in other components like the Nav.

Update Nav to change on scroll

Now that we have a custom React hook to track the user's scroll position, we can use that information to change the navigation bar's style when a user scrolls past 0px. First, import the useScrollPosition hook and set a new constant named scrollPosition that calls the hook and tracks the position:

export const Nav = () => {
const scrollPosition = useScrollPosition()
return <Popover className="sticky top-0 bg-white">...</Popover>
}

To help manage dynamic classes, we'll create a function inside the Nav called classNames:

export const Nav = () => {
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
const scrollPosition = useScrollPosition()
return <Popover className="sticky top-0 bg-white">...</Popover>
}

The className function's central role is to join strings passed into the function, which the join() array method is doing. However, you may have also noticed this weird filter(Boolean) method. This filter boolean trick checks to see if the value passed in is null before joining.

We'll use the classNames function to create dynamic classes in the Popover component inside the Nav by updating the following code:

export const Nav = () => {
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
const scrollPosition = useScrollPosition()
return (
<Popover
className={classNames(
scrollPosition > 0 ? 'shadow' : 'shadow-none',
'sticky top-0 z-20 bg-white transition-shadow',
)}
>
...
</Popover>
)
}

The first argument passed into the classNames function is a ternary operator that checks if the scroll position is greater than 0px. If true, the Tailwind class shadow will be used. Otherwise, the Tailwind class shadow-none will be used. The last argument is the rest of the styles that the Nav needs regardless of scroll position. However, notice that we added a transition-shadow class here. This class will add a smooth transition between the shadow and shadow-none changes.

Conclusion

You can improve the user experience of your websites and apps by adding a shadow or changing the navigation bar styles when a user scrolls. Luckily, adding such functionality to your React application is simple with a custom React hook and dynamic classes.