Scroll Down to See the Natural Hide Effect
You know what? Lorem ipsum is boring. Instead of meaningless placeholder text, let's show you the actual TypeScript source code that makes this header behavior work!
💡 Pro tip: Try clicking and dragging to scroll! This gives you precise control over scroll speed and direction, just like on mobile devices.
Fun fact: This header uses the default settings:
snapEagerness: 1.0
and scrollThreshold: 0
.
This entire script is only 1.0KB minified and has
zero dependencies. Pure TypeScript compiled to
vanilla JavaScript.
🔍 The Magic Behind This Header Animation
/**
* Attaches a natural hide/show behavior to a sticky element placed at the top.
*
* @param element - The HTML element to make naturally sticky
* @param options - Configuration options
* @param options.snapEagerness - How eagerly element snaps into sticky position (default: 1)
* @param options.scrollThreshold - Minimum scroll speed to trigger natural scroll-in effect (default: 0)
*/
export function naturalStickyTop(element: HTMLElement, options?: {
snapEagerness?: number;
scrollThreshold?: number
}) {
let lastScrollY = window.scrollY;
let isSticky = false;
let isHeaderAtTop = true;
const snapEagerness = options?.snapEagerness ?? 1;
const scrollThreshold = options?.scrollThreshold ?? 0;
const handleScroll = () => {
const currentScrollY = window.scrollY;
const elementRect = element.getBoundingClientRect();
const scrollStep = currentScrollY - lastScrollY;
const isElementVisible = elementRect.bottom > 0 && elementRect.top < window.innerHeight;
// Handle all relative mode logic first
if (!isSticky) {
// First priority: Check if element should switch to sticky
// Predict where element will be on next scroll event to avoid visual gaps
if (elementRect.top - snapEagerness * scrollStep >= 0) {
isSticky = true;
isHeaderAtTop = false;
element.style.position = 'sticky';
element.style.top = '0';
}
// Second priority: Check if scrolling up with enough speed to trigger scroll-in effect
else if (-scrollStep >= scrollThreshold && !isElementVisible) {
isHeaderAtTop = false;
element.style.position = 'relative';
// Position element just above viewport so it scrolls into view naturally
element.style.top = `${currentScrollY - element.offsetHeight}px`;
}
// Third priority: When header becomes invisible, move it to top of document
else if (!isHeaderAtTop && !isElementVisible) {
isHeaderAtTop = true;
element.style.position = 'relative';
element.style.top = '0px';
}
}
// Handle sticky mode logic - release from sticky when scrolling down
else if (scrollStep > 0) {
isSticky = false;
element.style.position = 'relative';
// Position element at current scroll position so it moves naturally with content
element.style.top = `${currentScrollY}px`;
}
lastScrollY = currentScrollY > 0 ? currentScrollY : 0;
};
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return { destroy: () => window.removeEventListener('scroll', handleScroll) };
}
Pretty neat, right? The key insight is that instead of using CSS transitions or JavaScript animations, we let the browser's natural scrolling do all the work. When you scroll down, the header just goes along for the ride. When you scroll up, we position it just above the viewport so it naturally scrolls back into view.
Keep scrolling to see more of how this works...
🎯 How It Works - The Natural Approach
Traditional sticky headers often use slide animations that can feel jarring and distracting:
- ❌ Sudden appearance/disappearance that breaks focus
- ❌ Fixed animation timing that doesn't match scroll speed
- ❌ Can cause layout shifts and visual interruptions
- ❌ Often require heavy animation libraries
- ❌ Trigger-based animations feel disconnected from user input
Our natural approach is different - it's designed to be less distracting:
- ✅ Elements move with the natural scroll flow
- ✅ Animation speed matches your scroll speed perfectly
- ✅ No jarring transitions or layout shifts
- ✅ Feels intuitive and unobtrusive - doesn't break user focus
- ✅ Movement feels like part of the content, not a separate animation
- ✅ Only 1.0KB with zero dependencies
⚙️ The Three States Explained
The header operates in three distinct modes:
position: sticky; top: 0
Header is visible and sticks to the top of the viewport
position: relative; top: currentScrollY
Header flows naturally with the content as you scroll down
position: relative; top: currentScrollY - elementHeight
Header is positioned just above the viewport, ready to scroll into view
The secret sauce is in the positioning logic. When you scroll down, we
switch from position: sticky
to
position: relative
and set the top position to the
current scroll position. This makes the element flow naturally with
the content as you scroll down.
When you scroll back up and the element isn't visible yet, we position
it just above the viewport. As you continue scrolling up, it naturally
comes into view, and once it reaches the top, we switch it back to
position: sticky
.
🌐 Browser Compatibility
Works in all modern browsers that support
position: sticky
(which is pretty much everything since
2017). No polyfills needed, no feature detection required - just
works.
📦 Bundle Size Comparison
Let's put that 1.0KB into perspective:
- Natural Sticky (top): 1.0KB minified
- AOS (Animate On Scroll): ~13KB minified
- Headroom.js: ~7KB minified
- Sticky-js: ~4KB minified
That's 87% smaller than the most popular alternatives, with better performance and more natural animations!
Try scrolling up and down a few more times to really feel the difference. Notice how the header movement feels completely natural, like it's just part of the page content flowing with your scroll. No artificial timing, no disconnected animations - just pure, natural movement.