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.
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};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.
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};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.
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.
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.
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.
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}, []);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.
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.
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