frontend.fyi

Auto-hiding Sticky Navigation With Framer Motion

Watch tutorial

With CSS position sticky, making a sticky navigation has become super easy. But what if you want to hide the navigation when the user scrolls down and show it when the user scrolls up? This is a common pattern that is actually a little bit harder to achieve.

In this recipe we’re using Framer Motion’s useScroll hook to get the current scroll position, and toggle the state of the navigation based on the scroll direction.

Watch the tutorial linked at the top of this recipe if you want to see how it’s done step by step.

The code

1
import { motion, useScroll, useMotionValueEvent } from "framer-motion";
2
import { useRef, useState } from "react";
3
4
function App() {
5
return (
6
<div className="min-h-[300vh] bg-gradient-to-b from-[rgba(255,255,255,.1)] to-[rgba(255,255,255,0)]">
7
<Nav />
8
</div>
9
);
10
}
11
12
const Nav = () => {
13
const [isHidden, setIsHidden] = useState(false);
14
const { scrollY } = useScroll();
15
const lastYRef = useRef(0);
16
17
useMotionValueEvent(scrollY, "change", (y) => {
18
const difference = y - lastYRef.current;
19
if (Math.abs(difference) > 50) {
20
setIsHidden(difference > 0);
21
22
lastYRef.current = y;
23
}
24
});
25
26
return (
27
<motion.div
28
animate={isHidden ? "hidden" : "visible"}
29
whileHover="visible"
30
onFocusCapture={() => setIsHidden(false)}
31
variants={{
32
hidden: {
33
y: "-90%",
34
},
35
visible: {
36
y: "0%",
37
},
38
}}
39
transition={{ duration: 0.2 }}
40
className="fixed top-0 z-10 flex w-full justify-center pt-3"
41
>
42
<nav className="flex justify-between gap-3 rounded-3xl bg-white p-5 *:rounded-xl *:border *:border-gray-200 *:px-7 *:py-2 *:transition-colors *:duration-300 hover:*:bg-gray-200 focus-visible:*:bg-gray-200">
43
<a href="#" className="bg-gray-200">
44
<svg
45
className="h-6 w-6"
46
fill="currentColor"
47
viewBox="0 0 24 24"
48
xmlns="http://www.w3.org/2000/svg"
49
>
50
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5-10-5-10 5z"></path>
51
</svg>
52
<span className="sr-only">Home</span>
53
</a>
54
<a href="#">Products</a>
55
<a href="#">Services</a>
56
<a href="#">About</a>
57
<a href="#">Contact</a>
58
</nav>
59
</motion.div>
60
);
61
};
62
63
export default App;

Changelog

  • 2024-06-06: Added the initial version of the component.
  • 2024-06-08: Updated snippet to match end result of tutorial: No additional (unnecessary) peeking variant.