Recently, I built a small React application that had a header, a navigation bar underneath and then the content. After scrolling past the header, the navigation bar was supposed to stay at the top and not scroll away.
Like so.
Making the navigation bar position: fixed; top 0;
was not going to cut it because the header had to come first unless we scrolled past the navigation bar.
I wanted to keep the component that controls the layout as simple as possible, so I abstracted the stickiness logic into a useSticky
hook. Here's how you'd use it.
import classNames from 'classnames';
import useSticky from './useSticky';
export default function App() {
const { sticky, stickyRef } = useSticky();
return (
<>
<header className="header">
<h1>Header</h1>
</header>
<nav ref={stickyRef} className={classNames('nav', { sticky })}>
Sticky nav
</nav>
<div
style={{
height: sticky ? `${stickyRef.current?.clientHeight}px` : '0px',
}}
/>
<main className="content" />
</>
);
}
You stick (no pun intended) the stickyRef
into the thing that you want to fix to the top after you scroll past it. Then, sticky
will indicate whether it should be fixed at the top or not. I then use that to apply a sticky
class.
.sticky {
position: fixed;
top: 0;
}
The extra div
below the nav
adds some extra padding so that the content does not immediately jump below the navigation bar when this becomes fixed at the top.
🌟 The magic happens in the useSticky
hook itself.
import { useEffect, useRef, useState } from 'react';
const useSticky = () => {
const stickyRef = useRef(null);
const [sticky, setSticky] = useState(false);
const [offset, setOffset] = useState(0);
useEffect(() => {
if (!stickyRef.current) {
return;
}
setOffset(stickyRef.current.offsetTop);
}, [stickyRef, setOffset]);
useEffect(() => {
const handleScroll = () => {
if (!stickyRef.current) {
return;
}
setSticky(window.scrollY > offset);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [setSticky, stickyRef, offset]);
return { stickyRef, sticky };
};
export default useSticky;
The first effect measures the vertical offset of the element we want to fix at the top after it's scrolled past. The second one listens to the scroll
event and determines whether we have scrolled past the element's top edge and it needs to become sticky.
🙌 Hope this is useful!