How to Build a Persistent React Header Component for Single-Page Applications

Implement a persistent React header by wrapping your layout in a scrollable container and applying position: sticky with top: 0 to the header element, optionally enhanced with React hooks for dynamic behaviors like collapsing on scroll.

Creating a persistent react header that remains visible while users navigate through different sections of a single-page application (SPA) is a common UI pattern. The facebook/react repository demonstrates this technique using CSS sticky positioning combined with React component architecture, providing a performant solution that requires minimal JavaScript overhead.

Why Use CSS Sticky Positioning for a Persistent Header

The most robust approach for building a persistent react header leverages CSS position: sticky rather than position: fixed. Sticky positioning keeps the element in the document flow while allowing it to stick to the viewport edge during scroll, eliminating the need to manually calculate offsets or manage padding-top on the body element.

According to the React source code in fixtures/view-transition/src/components/SwipeRecognizer.js, the core pattern involves creating a stickyStyle object that defines the sticky constraints:

const stickyStyle = {
  position: 'sticky',
  padding: '0px',
  margin: '0px',
  border: '0px',
  left: '0px',
  top: '0px',
  width: axis === 'x' ? '50%' : null,
  height: axis === 'y' ? '50%' : null,
  overflow: 'hidden',
};

This pattern can be adapted for a persistent header by applying similar styles to a <header> element.

Basic Implementation: Persistent Header with Sticky Positioning

Creating the Scrollable Container

For position: sticky to function correctly, the header must reside inside a scrollable container that establishes the scrolling context. In a React SPA, this typically means wrapping your entire application or main content area in a div with explicit overflow behavior.

Applying Sticky Styles to the Header

The header element requires position: sticky combined with top: 0 to ensure it sticks to the top of the viewport. A high zIndex ensures the header remains above scrolling content.

Here is a complete implementation based on patterns found in fixtures/dom/src/components/Header.js and the sticky box technique from the view-transition fixtures:

// src/components/PersistentHeader.js
import React, {useRef, useEffect} from 'react';

/**
 * A header that stays at the top of the page while scrolling.
 *
 * Props
 * -----
 *   children – content to render inside the header (e.g., logo, nav links)
 *   className – optional custom class for styling
 */
export default function PersistentHeader({children, className = ''}) {
  const containerRef = useRef(null);

  // OPTIONAL: Example of reacting to scroll direction
  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;

    let lastY = 0;
    const onScroll = () => {
      const dir = el.scrollTop > lastY ? 'down' : 'up';
      // For demo: add a class when scrolling down
      el.dataset.scrollDir = dir;
      lastY = el.scrollTop;
    };

    el.addEventListener('scroll', onScroll);
    return () => el.removeEventListener('scroll', onScroll);
  }, []);

  const containerStyle = {
    height: '100vh',
    overflowY: 'auto',
    WebkitOverflowScrolling: 'touch', // smoother scrolling on iOS
  };

  const headerStyle = {
    position: 'sticky',
    top: 0,
    left: 0,
    right: 0,
    zIndex: 10,
    background: '#fff',
    borderBottom: '1px solid #ddd',
    padding: '0.5rem 1rem',
  };

  return (
    <div ref={containerRef} style={containerStyle}>
      <header style={headerStyle} className={className}>
        {children}
      </header>

      {/* Example page sections */}
      <section style={{padding: '2rem'}}>
        <h2>Section 1</h2>
        <p>Content that scrolls beneath the persistent header...</p>
      </section>
      <section style={{padding: '2rem'}}>
        <h2>Section 2</h2>
        <p>Additional content demonstrating the sticky behavior...</p>
      </section>
    </div>
  );
}

Advanced Patterns for Persistent React Headers

Collapsing Header on Scroll Direction

For a more sophisticated persistent react header, you can combine the sticky CSS approach with React state to collapse the header when scrolling down and expand it when scrolling up. This pattern requires tracking scroll direction using a ref attached to the scrollable container.

The implementation follows the scroll listener pattern found in the React fixtures:

// src/components/CollapsingHeader.js
import React, {useRef, useEffect, useState} from 'react';

export default function CollapsingHeader({children}) {
  const containerRef = useRef(null);
  const [collapsed, setCollapsed] = useState(false);
  let lastScroll = 0;

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;

    const onScroll = () => {
      const delta = el.scrollTop - lastScroll;
      if (delta > 5 && !collapsed) setCollapsed(true);   // scrolling down
      else if (delta < -5 && collapsed) setCollapsed(false); // scrolling up
      lastScroll = el.scrollTop;
    };

    el.addEventListener('scroll', onScroll);
    return () => el.removeEventListener('scroll', onScroll);
  }, [collapsed]);

  const headerStyle = {
    position: 'sticky',
    top: 0,
    zIndex: 10,
    height: collapsed ? '40px' : '80px',
    transition: 'height 0.2s ease',
    background: '#fff',
    borderBottom: '1px solid #ddd',
    overflow: 'hidden',
  };

  const containerStyle = {
    height: '100vh',
    overflowY: 'auto',
  };

  return (
    <div ref={containerRef} style={containerStyle}>
      <header style={headerStyle}>{children}</header>
      {/* scrollable content follows */}
    </div>
  );
}

Integrating with React Router

When building a single-page application with React Router, the persistent header should remain visible across route transitions. The header component wraps the router outlet or renders the navigation links directly.

// src/App.js
import React from 'react';
import PersistentHeader from './components/PersistentHeader';
import {NavLink, Routes, Route} from 'react-router-dom';

function App() {
  return (
    <PersistentHeader>
      <div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between'}}>
        <img src="/logo.svg" alt="Logo" style={{height: 32}} />
        <nav>
          <NavLink to="/" end className={({isActive}) => isActive ? 'active' : ''}>Home</NavLink>
          <NavLink to="/about" className={({isActive}) => isActive ? 'active' : ''}>About</NavLink>
          <NavLink to="/contact" className={({isActive}) => isActive ? 'active' : ''}>Contact</NavLink>
        </nav>
      </div>
      
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </PersistentHeader>
  );
}

function Home() {
  return <section style={{padding: '2rem'}}><h2>Home Section</h2><p>Content...</p></section>;
}
function About() {
  return <section style={{padding: '2rem'}}><h2>About Section</h2><p>Content...</p></section>;
}
function Contact() {
  return <section style={{padding: '2rem'}}><h2>Contact Section</h2><p>Content...</p></section>;
}

export default App;

Key Source Files in the React Repository

The facebook/react repository contains reference implementations that demonstrate the sticky positioning technique used for persistent headers.

  • fixtures/view-transition/src/components/SwipeRecognizer.js – Contains the stickyStyle object that applies position: 'sticky' with top: 0 to create a sticky container. This file demonstrates the exact CSS-in-JS pattern used for persistent elements within scrollable areas.

  • fixtures/dom/src/components/Header.js – Provides a concrete implementation of a header component within the React DOM fixtures, showing structural patterns for navigation elements that remain visible during scroll.

  • packages/react/src/ReactFiberWorkLoop.new.js – While primarily concerned with React's internal scheduling, understanding the work loop helps optimize scroll-based state updates in persistent headers to avoid excessive re-renders during rapid scroll events.

Summary

  • Use CSS position: sticky rather than position: fixed to keep the header in document flow while maintaining visibility during scroll.
  • Wrap your application in a scrollable container (height: 100vh; overflow-y: auto) to establish the scrolling context required for sticky positioning.
  • Reference the React fixtures at fixtures/view-transition/src/components/SwipeRecognizer.js for the base implementation pattern using inline styles.
  • Enhance with React hooks by attaching ref listeners to the scrollable container for advanced behaviors like collapsing headers or scroll-direction detection.
  • Integrate seamlessly with React Router by placing navigation links inside the persistent header component to maintain visibility across route transitions.

Frequently Asked Questions

What is the difference between position: sticky and position: fixed for a React header?

position: sticky keeps the header in the normal document flow until it reaches a scroll threshold (typically top: 0), then behaves like a fixed element within its scrolling container. This eliminates the need to manually adjust body padding to account for header height. position: fixed removes the element from document flow entirely, requiring you to add margin or padding to the scrollable content to prevent overlap.

How do I prevent the persistent header from overlapping content in my React SPA?

Ensure your scrollable container has padding-top equal to the header's height, or place a spacer element immediately after the header within the same container. Since the header uses position: sticky and remains in document flow, content naturally stacks below it. If using position: fixed instead, you must manually calculate and apply padding-top to the scrollable wrapper to prevent content from sliding underneath the header.

Can I use CSS sticky positioning with React Portals?

Yes, but the portal's DOM node must reside inside the scrollable container that establishes the sticky context. If you render the header into a portal attached to document.body while the scrollable content is inside a nested div with overflow: auto, the sticky positioning will fail because the header is no longer a child of the scrolling ancestor. Keep the portal target inside the scrollable wrapper, or use position: fixed with manual coordinate calculations for headers rendered outside the main scroll container.

How do I optimize scroll performance when adding event listeners to a persistent header?

Attach the scroll listener to the scrollable container ref rather than the window object, and use passive: true event listeners where supported to prevent blocking the main thread. Debounce or throttle state updates that trigger React re-renders, or use CSS custom properties for visual changes that don't require React state updates. According to the React work loop implementation in packages/react/src/ReactFiberWorkLoop.new.js, minimizing synchronous updates during scroll events helps maintain smooth 60fps rendering.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →