Sticky Auto-Hiding Header with JS (or React)
Ever wondered how to create an auto-hide header on a page? Me too! Thet's why I built my own.
How to Create an Auto-Hiding Header
Setting the Constraints
Before development, I set some constraints:
- Ensure functionality without React.
- When using React, prevent unnecessary re-renders when the header shows or hides.
- Toggle visibility using a class.
- When the class is present, the header hides.
- Allow disabling the feature at any time.
- Implement scroll-based visibility rules:
- When scrolling down, hide the header after scrolling 75% of its height.
- When scrolling up, show the header after scrolling 50% of its height.
- When current scroll position is less than down threshold, show the header.
The TypeScript Solution
const ScrollDirection = { Up: 'up', Down: 'down' } as const;
type ScrollDirection = (typeof ScrollDirection)[keyof typeof ScrollDirection];
const DirectionThreshold = {
[ScrollDirection.Up]: 0.5,
[ScrollDirection.Down]: 0.75,
};
type HideOnScrollConfig = {
className: string;
};
const hideOnScroll = (
el: HTMLElement,
options?: Partial<HideOnScrollConfig>,
): AbortController => {
const config: HideOnScrollConfig = {
className: 'hide-on-scroll',
...(options || {}),
};
const abortController = new AbortController();
if (!el) {
return abortController;
}
let previousScrollPosition = window.scrollY;
let activeDirection: ScrollDirection = ScrollDirection.Up;
const headerHeight = el.scrollHeight;
const downThreshold = Math.round(headerHeight * DirectionThreshold.down);
const upThreshold = Math.round(headerHeight * DirectionThreshold.up);
window.addEventListener(
'scroll',
() => {
const currentPosition = window.scrollY;
const diff = currentPosition - previousScrollPosition;
const direction = diff > 0 ? ScrollDirection.Down : ScrollDirection.Up;
if (direction === activeDirection) {
previousScrollPosition = currentPosition;
return;
}
if (currentPosition < downThreshold) {
el.classList.remove(config.className);
return;
}
const diffAbs = Math.abs(diff);
if (direction === ScrollDirection.Down && diffAbs >= downThreshold) {
el.classList.add(config.className);
activeDirection = direction;
return;
}
if (direction === ScrollDirection.Up && diffAbs >= upThreshold) {
el.classList.remove(config.className);
activeDirection = direction;
}
},
{ signal: abortController.signal },
);
return abortController;
};
export default hideOnScroll;
How It Works
At first, the script sets the scroll position to the current position and sets the default scroll direction to "up." Since the function usually initializes when the page loads, this makes sense because users start at the top.
To follow the scroll visibility rules (50% and 75% thresholds), the script calculates these values based on the element’s height.
The core logic happens within the scroll event listener:
- The script calculates the difference between the previous and current scroll positions.
- It determines the scroll direction (up or down).
- If the direction hasn’t changed, it only updates the scroll position.
- If the direction changes:
- If near the top of the page, the header remains visible.
- Otherwise, the script compares the position change to the appropriate threshold and toggles the class if necessary.
The function returns an instance of AbortController
. It allows you to disable the effect by calling AbortController.abort()
. You’ll see this in the React example below.
Using It in React
To use this function in React, you need to use ref
and useEffect
. My header is rendered on the server. I created a HeaderWrapper
client component that takes <Header />
as a child. I also use Tailwind CSS. My "hide" class is -translate-y-full
, which corresponds to transform: translateY(-100%)
.
'use client';
import { PropsWithChildren, useEffect, useRef } from 'react';
import hideOnScroll from '@/components/Header/hideOnScroll';
type HeaderWrapperProps = PropsWithChildren;
const HeaderWrapper = ({ children }: HeaderWrapperProps) => {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) {
return;
}
const hideOnScrollController = hideOnScroll(ref.current, {
className: '-translate-y-full',
});
return () => hideOnScrollController.abort();
}, []);
return (
<header
className="py-2 sticky top-0 bg-surface z-10 transition-transform duration-500 border-b ease-out"
ref={ref}
>
{children}
</header>
);
};
export default HeaderWrapper;
Here, hideOnScroll
attaches to the header’s ref
, and AbortController.abort()
runs during cleanup to remove the event listener.
Final Thoughts
This guide walks through creating an auto-hiding header using JavaScript and React. By calculating scroll differences and applying CSS classes dynamically, you can create a seamless and responsive effect. The solution is lightweight, efficient, and easy to integrate. With a few more lines of code, you could add configuration options to adjust the visibility thresholds dynamically. This would allow for greater flexibility, making the behavior adaptable to different layouts and user preferences.