Scroll Direction Detection
Automatically detects scroll direction and triggers animations based on user-defined criteria.
The useHideOnScroll
hook creates smooth hide/show animations for UI components based on scroll direction. Perfect for headers, navigation bars, toolbars, and floating action buttons that should appear or disappear based on user scroll behavior.
Scroll Direction Detection
Automatically detects scroll direction and triggers animations based on user-defined criteria.
Bounce Protection
Includes intelligent bounce detection to prevent unwanted animations at scroll edges.
Dual Orientation Support
Works with both vertical and horizontal scrolling with customizable hide directions.
Smooth Animations
Uses React Native Reanimated for 60fps smooth animations with customizable timing.
Programmatic Control
Provides forceShow
and forceHide
methods for manual control over visibility.
Threshold Control
Configurable scroll threshold to prevent accidental triggers from small movements.
height: number
Required - Height of the component to be hidden (in pixels). This determines the translation distance for the hide animation.
duration?: number
Animation duration in milliseconds
Default: 300
threshold?: number
Minimum scroll distance before animation starts
Default: 10
scrollDirection?: ScrollDirectionType
Direction of scroll that triggers hiding
Options: 'up' | 'down' | 'left' | 'right'
Default: 'down'
hideDirection?: HideDirectionType
Direction in which component will be hidden
Options: 'up' | 'down' | 'left' | 'right'
Default: 'down'
orientation?: string
Scroll orientation
Options: 'vertical' | 'horizontal'
Default: 'vertical'
animatedStyle
Animated style object containing transform and opacity properties to apply to your component.
onScroll
Event handler function to pass to your ScrollView or FlatList’s onScroll
prop.
sharedScrollVertical
Shared value for vertical scroll position - useful for creating derived animations.
sharedScrollHorizontal
Shared value for horizontal scroll position - useful for creating derived animations.
forceShow
Function to programmatically show the component regardless of scroll position.
forceHide
Function to programmatically hide the component regardless of scroll position.
import React from 'react';import { ScrollView, View, Text } from 'react-native';import Animated from 'react-native-reanimated';import { useHideOnScroll } from './useHideOnScroll';
function HeaderExample() {const { animatedStyle, onScroll } = useHideOnScroll({ height: 60, scrollDirection: 'down', // Hide when scrolling down hideDirection: 'up', // Hide by sliding up});
return ( <View style={{ flex: 1 }}> <Animated.View style={[styles.header, animatedStyle]}> <Text style={styles.headerText}>Header</Text> </Animated.View>
<ScrollView onScroll={onScroll} scrollEventThrottle={16} showsVerticalScrollIndicator={false} > {/* Your content */} {Array.from({ length: 50 }, (_, i) => ( <View key={i} style={styles.item}> <Text>Item {i + 1}</Text> </View> ))} </ScrollView> </View>);}
const styles = {header: { height: 60, backgroundColor: '#007AFF', justifyContent: 'center', alignItems: 'center', position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1000,},headerText: { color: 'white', fontSize: 18, fontWeight: 'bold',},item: { padding: 20, borderBottomWidth: 1, borderBottomColor: '#eee',},};
import React from 'react';import { ScrollView, View, Text, TouchableOpacity } from 'react-native';import Animated from 'react-native-reanimated';import { useHideOnScroll } from './useHideOnScroll';
function TabBarExample() {const { animatedStyle, onScroll } = useHideOnScroll({ height: 80, scrollDirection: 'down', // Hide when scrolling down hideDirection: 'down', // Hide by sliding down threshold: 20, // Higher threshold for tab bar});
return ( <View style={{ flex: 1 }}> <ScrollView onScroll={onScroll} scrollEventThrottle={16} contentContainerStyle={{ paddingBottom: 100 }} > {/* Your content */} {Array.from({ length: 50 }, (_, i) => ( <View key={i} style={styles.item}> <Text>Content Item {i + 1}</Text> </View> ))} </ScrollView>
<Animated.View style={[styles.tabBar, animatedStyle]}> <TouchableOpacity style={styles.tab}> <Text>Home</Text> </TouchableOpacity> <TouchableOpacity style={styles.tab}> <Text>Search</Text> </TouchableOpacity> <TouchableOpacity style={styles.tab}> <Text>Profile</Text> </TouchableOpacity> </Animated.View> </View>);}
const styles = {tabBar: { height: 80, backgroundColor: 'white', flexDirection: 'row', position: 'absolute', bottom: 0, left: 0, right: 0, borderTopWidth: 1, borderTopColor: '#eee',},tab: { flex: 1, justifyContent: 'center', alignItems: 'center',},item: { padding: 20, borderBottomWidth: 1, borderBottomColor: '#eee',},};
import React, { useEffect, useState } from 'react';import { ScrollView, View, Text, TouchableOpacity } from 'react-native';import Animated from 'react-native-reanimated';import { useHideOnScroll } from './useHideOnScroll';
function FloatingActionButtonExample() {const [isSearchMode, setIsSearchMode] = useState(false);
const { animatedStyle, onScroll, forceShow, forceHide } = useHideOnScroll({ height: 56, duration: 250, threshold: 15, scrollDirection: 'down', hideDirection: 'down',});
// Force show FAB when in search modeuseEffect(() => { if (isSearchMode) { forceShow(); }}, [isSearchMode, forceShow]);
return ( <View style={{ flex: 1 }}> <View style={styles.searchToggle}> <TouchableOpacity onPress={() => setIsSearchMode(!isSearchMode)} style={styles.toggleButton} > <Text>{isSearchMode ? 'Exit Search' : 'Search Mode'}</Text> </TouchableOpacity> </View>
<ScrollView onScroll={onScroll} scrollEventThrottle={16} contentContainerStyle={{ paddingBottom: 80 }} > {Array.from({ length: 100 }, (_, i) => ( <View key={i} style={styles.item}> <Text>Scrollable Item {i + 1}</Text> </View> ))} </ScrollView>
<Animated.View style={[styles.fab, animatedStyle]}> <TouchableOpacity style={styles.fabButton}> <Text style={styles.fabText}>+</Text> </TouchableOpacity> </Animated.View> </View>);}
const styles = {searchToggle: { padding: 16, backgroundColor: '#f5f5f5', alignItems: 'center',},toggleButton: { backgroundColor: '#007AFF', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8,},fab: { position: 'absolute', bottom: 20, right: 20, width: 56, height: 56,},fabButton: { width: 56, height: 56, borderRadius: 28, backgroundColor: '#FF6B6B', justifyContent: 'center', alignItems: 'center', elevation: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8,},fabText: { color: 'white', fontSize: 24, fontWeight: 'bold',},item: { padding: 20, borderBottomWidth: 1, borderBottomColor: '#eee',},};
import React from 'react';import { ScrollView, View, Text, Dimensions } from 'react-native';import Animated from 'react-native-reanimated';import { useHideOnScroll } from './useHideOnScroll';
const { width } = Dimensions.get('window');
function HorizontalScrollExample() {const { animatedStyle, onScroll } = useHideOnScroll({ height: 60, // Component width for horizontal duration: 200, threshold: 10, scrollDirection: 'right', // Hide when scrolling right hideDirection: 'right', // Hide by sliding right orientation: 'horizontal',});
return ( <View style={{ flex: 1 }}> <View style={styles.header}> <Text style={styles.headerText}>Horizontal Scroll Demo</Text> </View>
<ScrollView horizontal onScroll={onScroll} scrollEventThrottle={16} showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingRight: 100 }} > {Array.from({ length: 20 }, (_, i) => ( <View key={i} style={styles.card}> <Text style={styles.cardText}>Card {i + 1}</Text> </View> ))} </ScrollView>
<Animated.View style={[styles.sidePanel, animatedStyle]}> <Text style={styles.panelText}>Side Panel</Text> </Animated.View> </View>);}
const styles = {header: { height: 60, backgroundColor: '#007AFF', justifyContent: 'center', alignItems: 'center',},headerText: { color: 'white', fontSize: 18, fontWeight: 'bold',},card: { width: width * 0.8, height: 200, backgroundColor: '#f0f0f0', marginHorizontal: 10, borderRadius: 12, justifyContent: 'center', alignItems: 'center',},cardText: { fontSize: 16, fontWeight: '600',},sidePanel: { position: 'absolute', right: 0, top: 60, bottom: 0, width: 60, backgroundColor: '#FF6B6B', justifyContent: 'center', alignItems: 'center',},panelText: { color: 'white', fontSize: 12, fontWeight: 'bold', transform: [{ rotate: '90deg' }],},};
const { animatedStyle, onScroll } = useHideOnScroll({height: 60,scrollDirection: 'down', // Hide when scrolling downhideDirection: 'up', // Slide up to hidethreshold: 10,duration: 300,});
const { animatedStyle, onScroll } = useHideOnScroll({height: 80,scrollDirection: 'down', // Hide when scrolling downhideDirection: 'down', // Slide down to hidethreshold: 20, // Higher thresholdduration: 250,});
const { animatedStyle, onScroll } = useHideOnScroll({height: 200, // Actually width for horizontalscrollDirection: 'right', // Hide when scrolling righthideDirection: 'left', // Slide left to hideorientation: 'horizontal',threshold: 15,duration: 300,});
const { animatedStyle, onScroll, forceShow } = useHideOnScroll({height: 56,scrollDirection: 'down',hideDirection: 'down',threshold: 25, // Less sensitiveduration: 200, // Quick animation});
function CustomAnimationExample() {const { animatedStyle, onScroll, sharedScrollVertical} = useHideOnScroll({ height: 60, hideDirection: 'up',});
// Create additional animation based on scroll positionconst headerBackgroundStyle = useAnimatedStyle(() => { const opacity = interpolate( sharedScrollVertical.value, [0, 100], [0, 1], Extrapolation.CLAMP );
return { backgroundColor: `rgba(0, 122, 255, ${opacity})`, };});
return ( <View style={{ flex: 1 }}> <Animated.View style={[styles.header, animatedStyle, headerBackgroundStyle]}> <Text style={styles.headerText}>Dynamic Header</Text> </Animated.View>
<ScrollView onScroll={onScroll} scrollEventThrottle={16}> {/* Content */} </ScrollView> </View>);}
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
function GestureControlExample() {const { animatedStyle, onScroll, forceShow, forceHide } = useHideOnScroll({ height: 60, hideDirection: 'up',});
const tapGesture = Gesture.Tap() .numberOfTaps(2) .onEnd(() => { // Double tap to toggle visibility runOnJS(forceShow)(); });
const longPressGesture = Gesture.LongPress() .minDuration(500) .onEnd(() => { // Long press to hide runOnJS(forceHide)(); });
const composed = Gesture.Race(tapGesture, longPressGesture);
return ( <View style={{ flex: 1 }}> <GestureDetector gesture={composed}> <Animated.View style={[styles.header, animatedStyle]}> <Text style={styles.headerText}> Double tap to show, Long press to hide </Text> </Animated.View> </GestureDetector>
<ScrollView onScroll={onScroll} scrollEventThrottle={16}> {/* Content */} </ScrollView> </View>);}
Set scrollEventThrottle to 16 for 60fps smooth animations on ScrollView/FlatList
Use appropriate threshold values to prevent excessive animation triggers from small scroll movements
Consider component positioning - Use position: 'absolute'
for better performance with transform animations
Optimize content rendering - Use FlatList for large datasets instead of ScrollView with many children
Test on various devices - Lower-end devices may need higher thresholds or longer durations
Use runOnJS sparingly - Keep worklet functions pure when possible for better performance
Animation not triggering
Common Causes:
scrollEventThrottle={16}
on ScrollViewSolutions:
Jerky animations
Common Causes:
Solutions:
Animations at scroll edges
Common Causes:
Solutions:
TypeScript errors
Common Causes:
Solutions:
function FlatListExample() {const { animatedStyle, onScroll } = useHideOnScroll({ height: 60, hideDirection: 'up', threshold: 20,});
const renderItem = ({ item, index }) => ( <View style={styles.item}> <Text>Item {index + 1}</Text> </View>);
return ( <View style={{ flex: 1 }}> <Animated.View style={[styles.header, animatedStyle]}> <Text style={styles.headerText}>FlatList Header</Text> </Animated.View>
<FlatList data={Array.from({ length: 1000 }, (_, i) => ({ id: i }))} renderItem={renderItem} keyExtractor={(item) => item.id.toString()} onScroll={onScroll} scrollEventThrottle={16} contentContainerStyle={{ paddingTop: 60 }} /> </View>);}
import { useFocusEffect } from '@react-navigation/native';
function NavigationExample() {const { animatedStyle, onScroll, forceShow } = useHideOnScroll({ height: 60, hideDirection: 'up',});
// Always show header when screen comes into focususeFocusEffect( React.useCallback(() => { forceShow(); }, [forceShow]));
return ( <View style={{ flex: 1 }}> <Animated.View style={[styles.header, animatedStyle]}> <Text style={styles.headerText}>Navigation Screen</Text> </Animated.View>
<ScrollView onScroll={onScroll} scrollEventThrottle={16}> {/* Screen content */} </ScrollView> </View>);}