Scroll Direction: Hide Header

Matt Perry

In this tutorial, we're going to build the Scroll Direction: Hide Header example step-by-step.

This example is rated intermediate difficulty, which means we'll spend some time explaining the Motion APIs we've chosen to use, but it assumes familiarity with JavaScript as a language.

Here's a live demo of the example we're going to be creating:

Loading...

A common UX pattern: hide the header when scrolling down to maximize content space, then reveal it when scrolling up so navigation is always accessible.

How it works

The useScroll hook provides a scrollY motion value that tracks the current scroll position. We use useMotionValueEvent to listen for changes and compare the current position to the previous one.

const { scrollY } = useScroll()
const [hidden, setHidden] = useState(false)

useMotionValueEvent(scrollY, "change", (current) => {
    const previous = scrollY.getPrevious() ?? 0
    if (current > previous && current > 150) {
        setHidden(true)
    } else {
        setHidden(false)
    }
})

The logic

  • current > previous — user is scrolling down

  • current > 150 — only hide after scrolling past 150px (so it doesn't hide immediately)

  • Otherwise, show the header (user scrolling up or at top)

Animating the header

The header animates between visible and hidden using the animate prop:

<motion.header
    animate={{ y: hidden ? "-100%" : "0%" }}
    transition={{ duration: 0.3, ease: "easeInOut" }}
>

Using -100% moves the header up by its own height, completely hiding it above the viewport.

Why useMotionValueEvent?

You might be tempted to use useEffect with scrollY.get(), but that would cause unnecessary re-renders. useMotionValueEvent subscribes directly to the motion value and only triggers when the value changes, making it more performant.

Accessibility

Consider adding prefers-reduced-motion support to disable the animation for users who prefer reduced motion:

@media (prefers-reduced-motion: reduce) {
    .header {
        transition: none;
    }
}

Variations

Show on scroll up only after threshold

if (current > previous && current > 150) {
    setHidden(true)
} else if (current < previous) {
    setHidden(false)
}
// Header stays hidden if user stops scrolling

Smooth spring animation

<motion.header
    animate={{ y: hidden ? "-100%" : "0%" }}
    transition={{ type: "spring", stiffness: 300, damping: 30 }}
>

Motion is supported by the best in the industry.