Let's build a production-grade header component that hides when scrolling down and reappears instantly when scrolling up, without causing layout thrashing or jank.
What you'll learn:
- Tracking window scroll position efficiently using React hooks
- Optimizing scroll event listeners with
requestAnimationFrame - Structuring a responsive layout with Tailwind CSS
- Managing accessibility for sticky navigation elements
Setting Up the Base Header Layout
A robust header needs a fixed or sticky positioning model that anchors smoothly to the top of the viewport. We will start by creating a container component with proper semantic markup and Tailwind utility classes.
import React from 'react';
export function Header() {
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200">
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
<div className="font-bold text-lg text-slate-900 tracking-tight">
Acme Corp
</div>
<nav className="hidden md:flex items-center gap-6 text-sm font-medium text-slate-600">
<a href="#features" className="hover:text-slate-900 transition-colors">Features</a>
<a href="#pricing" className="hover:text-slate-900 transition-colors">Pricing</a>
<a href="#company" className="hover:text-slate-900 transition-colors">Company</a>
</nav>
<div>
<button className="bg-slate-900 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 transition-colors">
Get Started
</button>
</div>
</div>
</header>
);
}
Speed Note: Using
backdrop-blur-mdalongsidebg-white/80creates a modern frosted glass effect, but keep an eye on mobile GPU performance during rapid scrolls.
Adding Scroll Direction Detection
To hide the header on scroll down and show it on scroll up, we need a custom hook that tracks scroll delta efficiently.
import { useState, useEffect } from 'react';
export function useScrollDirection() {
const [scrollDirection, setScrollDirection] = useState<'up' | 'down'>('up');
const [prevOffset, setPrevOffset] = useState(0);
useEffect(() => {
let ticking = false;
const updateScrollDirection = () => {
const currentOffset = window.pageYOffset;
if (Math.abs(currentOffset - prevOffset) < 10) {
ticking = false;
return;
}
if (currentOffset > prevOffset && currentOffset > 100) {
setScrollDirection('down');
} else if (currentOffset < prevOffset) {
setScrollDirection('up');
}
setPrevOffset(currentOffset);
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDirection);
ticking = true;
}
};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, [prevOffset]);
return scrollDirection;
}
Gotcha: Never attach heavy DOM reads or state setters directly to raw scroll events. Always throttle or debounce updates using
requestAnimationFrameto maintain a steady 60fps frame rate.
Integrating Transform Animations
Now we combine our scroll direction hook with Tailwind transition classes to slide the header cleanly out of view.
import React from 'react';
import { useScrollDirection } from './useScrollDirection';
export function InteractiveHeader() {
const scrollDirection = useScrollDirection();
return (
<header
className={`fixed top-0 left-0 right-0 z-50 bg-white border-b border-slate-200 transition-transform duration-300 ${
scrollDirection === 'down' ? '-translate-y-full' : 'translate-y-0'
}`}
>
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
<span className="font-bold text-slate-900">AppUI</span>
<div className="flex gap-4 text-sm">
<a href="#docs" className="text-slate-600">Docs</a>
<a href="#blog" className="text-slate-600">Blog</a>
</div>
</div>
</header>
);
}
Handling Accessibility and Focus States
When hiding elements dynamically, screen readers and keyboard users can get disoriented if focus moves into hidden containers. Ensure interactive links remain accessible.
import React, { useState } from 'react';
export function AccessibleHeader() {
const [isOpen, setIsOpen] = useState(false);
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-white border-b border-slate-200">
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
<a href="/" className="font-bold text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 rounded">
Brand
</a>
<button
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-label="Toggle navigation menu"
className="md:hidden p-2 text-slate-600 focus:outline-none focus:ring-2 focus:ring-slate-900 rounded"
>
Menu
</button>
</div>
</header>
);
}
Accessibility Note: Always pair hamburger toggle buttons with
aria-expandedandaria-labelattributes so screen reader users know the current state of the navigation panel.
Summary
Building a performant header is all about combining clean CSS layout transitions with optimized JavaScript scroll listeners. By offloading scroll calculations to requestAnimationFrame and managing layout transforms cleanly, you avoid layout thrashing entirely.
Next Steps:
- Add a scroll-position threshold where the header gains a shadow and solid background.
- Test keyboard navigation flow across mobile drawer menus.
- Benchmark your scroll listener inside Chrome DevTools to ensure zero dropped frames.