Using React Hooks To Create Sticky Headers

Master the art of creating sticky navigation with React Hooks, from simple implementations to advanced scroll-linked animations with performance optimization and accessibility support.

Sticky headers are one of the most common yet essential UI patterns in modern web development. When a user scrolls down your page, keeping navigation visible improves usability and keeps key actions accessible at all times. While you can achieve this with pure CSS using position: sticky, React Hooks give you fine-grained control over when and how elements become sticky, enabling sophisticated behaviors like hide-on-scroll, scroll-triggered animations, and conditional sticky states based on viewport position.

In this guide, we'll explore multiple approaches to creating sticky headers with React Hooks, ranging from simple CSS class toggling to advanced scroll-linked animations. Whether you need a straightforward navigation bar or a dynamic header that responds to scroll direction, you'll find a pattern that fits your needs.

The Foundation: Basic Sticky Header with React Hooks

The most straightforward approach to creating a sticky header involves tracking the user's scroll position and conditionally applying CSS classes based on whether the header has crossed the viewport threshold. This method gives you complete control over the sticky behavior and works with any styling approach.

Basic Sticky Header Implementation
1import { useEffect, useRef, useState } from 'react';2 3const StickyHeader = () => {4 const [isSticky, setSticky] = useState(false);5 const headerRef = useRef(null);6 7 const handleScroll = () => {8 if (headerRef.current) {9 setSticky(headerRef.current.getBoundingClientRect().top <= 0);10 }11 };12 13 useEffect(() => {14 window.addEventListener('scroll', handleScroll);15 return () => window.removeEventListener('scroll', handleScroll);16 }, []);17 18 return (19 <header ref={headerRef} className={isSticky ? 'sticky' : ''}>20 {/* Header content */}21 </header>22 );23};
CSS for Sticky Behavior
1header.sticky {2 position: fixed;3 top: 0;4 left: 0;5 right: 0;6 z-index: 1000;7}

How It Works

The core logic relies on three React Hooks working together:

  • useRef creates a reference to the header element so we can measure its position
  • useState tracks whether the header should be sticky (boolean value)
  • useEffect adds a scroll event listener when the component mounts and removes it on unmount
  • getBoundingClientRect().top tells us the header's position relative to the viewport

As demonstrated in Pantaley's guide on creating sticky headers with React Hooks, this approach uses pure React without any external dependencies. For projects using TypeScript, you can add proper type annotations to improve code safety.

Creating a Reusable useSticky Hook

One of React's greatest strengths is the ability to extract and reuse component logic through custom hooks. By moving the sticky detection logic into its own hook, you can apply sticky behavior to any element without duplicating code. This approach keeps your components cleaner and more focused on rendering rather than scroll event management.

Reusable useSticky Hook
1const useSticky = (offset = 0) => {2 const [isSticky, setSticky] = useState(false);3 const elementRef = useRef(null);4 5 useEffect(() => {6 if (!elementRef.current) return;7 8 const handleScroll = () => {9 const element = elementRef.current;10 const elementTop = element.getBoundingClientRect().top;11 setSticky(elementTop - offset <= 0);12 };13 14 window.addEventListener('scroll', handleScroll);15 handleScroll(); // Check initial state16 17 return () => window.removeEventListener('scroll', handleScroll);18 }, [offset]);19 20 return { isSticky, elementRef };21};
Using the Custom Hook
1const Navigation = () => {2 const { isSticky, elementRef } = useSticky(10);3 4 return (5 <nav ref={elementRef} className={isSticky ? 'sticky-nav' : ''}>6 {/* Navigation items */}7 </nav>8 );9};

As illustrated in David Saltares's implementation of a sticky nav bar using React hooks, this pattern provides a clean abstraction that can be reused across multiple components in your application. Following web development best practices ensures your custom hooks are maintainable and performant.

Dynamic Sticky Headers: Hide on Scroll

Advanced sticky headers often implement a 'smart' behavior where they hide when scrolling down (to maximize content visibility) and reappear when scrolling up. This pattern, popularized by mobile apps like Twitter and YouTube, provides an excellent user experience for content-heavy pages.

Scroll Direction Detection

The key to this behavior is detecting scroll direction, which requires tracking the previous scroll position and comparing it to the current position. Positive delta means scrolling down (hide header), negative delta means scrolling up (show header). A threshold value prevents jittery behavior with small scroll movements.

Following the approach from Kieran Roberts's tutorial on scroll-translated dynamic sticky navbar, we can implement smooth hide-on-scroll behavior. These patterns also work well in React Native applications with minor adjustments for mobile scroll handling.

useStickyHeader with Scroll Direction
1const useStickyHeader = () => {2 const scrollRef = useRef({ prevScrollTop: 0, animation: undefined });3 const headerRef = useRef(null);4 const [prefersReducedMotion, setPrefersReducedMotion] = useState(true);5 6 const getScrollDistance = ({ scrollY }) => {7 const { prevScrollTop } = scrollRef.current;8 return scrollY - prevScrollTop;9 };10 11 const calculateTranslateValue = ({ headerTop, scrollDistance }) => {12 const navHeight = (headerRef.current?.offsetHeight || 0) + 30;13 return Math.max(14 Math.min(15 headerTop + (scrollDistance < 0 ? Math.abs(scrollDistance) : -Math.abs(scrollDistance)),16 017 ),18 -navHeight19 );20 };21 22 const onTranslate = () => {23 scrollRef.current.animation = requestAnimationFrame(() => {24 const scrollY = window.scrollY;25 const scrollDistance = getScrollDistance({ scrollY });26 const headerTop = headerRef.current?.getBoundingClientRect().top || 0;27 const translateAmount = calculateTranslateValue({ headerTop, scrollDistance });28 29 if (headerRef.current) {30 headerRef.current.style.transform = `translateY(${translateAmount}px)`;31 }32 scrollRef.current.prevScrollTop = scrollY;33 });34 };35 36 useEffect(() => {37 if (prefersReducedMotion) return;38 window.addEventListener('scroll', onTranslate);39 return () => {40 window.removeEventListener('scroll', onTranslate);41 if (scrollRef.current.animation) {42 cancelAnimationFrame(scrollRef.current.animation);43 }44 };45 }, [prefersReducedMotion]);46 47 useEffect(() => {48 const mediaQuery = window.matchMedia('(prefers-reduced-motion: no-preference)');49 setPrefersReducedMotion(!mediaQuery.matches);50 const updateMotionSettings = (event) => {51 setPrefersReducedMotion(!event.matches);52 };53 mediaQuery.addEventListener('change', updateMotionSettings);54 return () => mediaQuery.removeEventListener('change', updateMotionSettings);55 }, []);56 57 return headerRef;58};

Accessibility: Supporting prefers-reduced-motion

Many users have vestibular disorders or motion sensitivity that makes animated interfaces uncomfortable. The prefers-reduced-motion media query allows you to detect these preferences and provide a static alternative that works reliably without animation.

Reduced Motion Support
1const StickyHeader = () => {2 const headerRef = useRef(null);3 const [shouldAnimate, setShouldAnimate] = useState(false);4 5 useEffect(() => {6 const mediaQuery = window.matchMedia('(prefers-reduced-motion: no-preference)');7 setShouldAnimate(mediaQuery.matches);8 const handleChange = (event) => {9 setShouldAnimate(event.matches);10 };11 mediaQuery.addEventListener('change', handleChange);12 return () => mediaQuery.removeEventListener('change', handleChange);13 }, []);14 15 if (!shouldAnimate) {16 return (17 <header ref={headerRef} className="static-sticky">18 {/* Header content */}19 </header>20 );21 }22 23 return <AnimatedStickyHeader />;24};

As demonstrated in Kieran Roberts's motion-sensitive implementation, you can detect reduced motion preferences and provide appropriate fallbacks for users who prefer reduced motion. Our web development services include comprehensive accessibility audits to ensure your applications meet WCAG standards.

Using Framer Motion's useScroll

For projects using Framer Motion, the library's useScroll hook provides a declarative way to create scroll-linked animations that integrate seamlessly with the animation system. This approach offers more sophisticated effects than direct DOM manipulation.

Framer Motion useScroll Example
1import { useScroll, useSpring, useTransform, motion } from 'framer-motion';2 3const ScrollProgress = () => {4 const { scrollYProgress } = useScroll();5 const scaleX = useSpring(scrollYProgress, {6 stiffness: 100,7 damping: 30,8 restDelta: 0.0019 });10 11 return (12 <motion.div13 style={{14 scaleX,15 position: 'fixed',16 top: 0,17 left: 0,18 right: 0,19 height: '4px',20 background: '#007bff',21 transformOrigin: '0%',22 }}23 />24 );25};

The Framer Motion documentation on useScroll provides comprehensive details on creating scroll-linked animations with motion values and transforms. This technique is particularly effective for React web applications that require polished, performant animations.

Performance Best Practices

Scroll event handlers can significantly impact performance if not implemented carefully. A scroll event can fire dozens of times per second, and each handler execution contributes to frame time. Poorly implemented scroll handlers can cause janky scrolling, dropped frames, and unresponsive pages.

Performance Pitfall
1// BAD: Direct state updates on every scroll event2useEffect(() => {3 const handleScroll = () => {4 setScrollY(window.scrollY); // Triggers re-render on every scroll5 };6 window.addEventListener('scroll', handleScroll);7 return () => window.removeEventListener('scroll', handleScroll);8}, []);
Performance Optimized Implementation
1const StickyHeader = () => {2 const scrollRef = useRef(0);3 const [isSticky, setSticky] = useState(false);4 5 useEffect(() => {6 const handleScroll = () => {7 scrollRef.current = window.scrollY;8 setSticky(prev => scrollRef.current > 100 && !prev);9 };10 window.addEventListener('scroll', handleScroll, { passive: true });11 return () => window.removeEventListener('scroll', handleScroll);12 }, []);13 14 return <header className={isSticky ? 'sticky' : ''}>{/* content */}</header>;15};

Common Pitfalls and Solutions

Pitfall 1: Memory Leaks

Always remove event listeners on unmount to prevent memory leaks. The cleanup function in useEffect ensures that dangling references don't accumulate.

Pitfall 2: Stale Closures

State captured in closures may be outdated. Ensure handlers are self-contained and properly handle dependencies.

Pitfall 3: Content Jump

When an element becomes fixed, it removes from document flow. Use a spacer element to maintain height and prevent layout shifts.

Fixing Content Jump with Spacer
1// CORRECT: Spacer element maintains height2<header ref={headerRef} className={isSticky ? 'sticky' : ''}>3 Logo - Nav4</header>5{isSticky && <div style={{ height: headerRef.current?.offsetHeight }} />}6<main>Content stays in place</main>

Conclusion

Creating sticky headers with React Hooks offers a powerful middle ground between simple CSS solutions and complex JavaScript implementations. Whether you need a straightforward navigation bar or a sophisticated smart header that responds to scroll direction, React's hook system provides the primitives you need.

Start with the basic useState + useEffect approach if you need simple sticky behavior. Extract your logic into a custom hook for reusability across components. Implement scroll direction detection for hide-on-scroll effects. And always respect accessibility preferences with reduced motion support.

The patterns covered in this guide provide a foundation you can adapt to any project's needs, from simple marketing sites to complex web applications. Remember to prioritize performance by cleaning up event listeners and batching updates, and always test your implementations across different devices and browsers. If you need assistance implementing these patterns, our web development team is ready to help build performant, accessible React applications.

Key Takeaways

Basic Implementation

useState + useEffect + useRef provides foundation for sticky behavior

Custom Hooks

Extract logic into reusable useSticky hook for clean component code

Performance

requestAnimationFrame and passive listeners optimize scroll handling

Accessibility

prefers-reduced-motion media query provides static fallback

Framer Motion

useScroll enables declarative scroll-linked animations

Common Pitfalls

Proper cleanup, stale closures, and content jump fixes

Need Help Building Modern React Applications?

Our team specializes in creating performant, accessible web interfaces using React and modern best practices.