Skip to content

useHideOnScroll Hook

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.

HeaderExample.tsx
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',
},
};
TabBarExample.tsx
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',
},
};

Floating Action Button with Custom Control

Section titled “Floating Action Button with Custom Control”
FloatingActionButtonExample.tsx
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 mode
useEffect(() => {
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',
},
};
HorizontalScrollExample.tsx
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 down
hideDirection: 'up', // Slide up to hide
threshold: 10,
duration: 300,
});
Custom Animation with Shared Values
function CustomAnimationExample() {
const {
animatedStyle,
onScroll,
sharedScrollVertical
} = useHideOnScroll({
height: 60,
hideDirection: 'up',
});
// Create additional animation based on scroll position
const 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>
);
}
Gesture-Based Control
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>
);
}
  1. Set scrollEventThrottle to 16 for 60fps smooth animations on ScrollView/FlatList

  2. Use appropriate threshold values to prevent excessive animation triggers from small scroll movements

  3. Consider component positioning - Use position: 'absolute' for better performance with transform animations

  4. Optimize content rendering - Use FlatList for large datasets instead of ScrollView with many children

  5. Test on various devices - Lower-end devices may need higher thresholds or longer durations

  6. Use runOnJS sparingly - Keep worklet functions pure when possible for better performance

Animation not triggering

Common Causes:

  • Missing scrollEventThrottle={16} on ScrollView
  • Threshold value too high
  • Component not properly positioned

Solutions:

  • Add scrollEventThrottle prop
  • Lower threshold value
  • Ensure proper absolute positioning

Jerky animations

Common Causes:

  • High scrollEventThrottle value
  • Complex style calculations
  • Too many re-renders

Solutions:

  • Use scrollEventThrottle=16
  • Optimize style calculations
  • Use React.memo for complex components

Animations at scroll edges

Common Causes:

  • Bounce effects triggering animations
  • Content size detection issues

Solutions:

  • The hook includes built-in bounce protection
  • Ensure proper content sizing
  • Check ScrollView contentContainerStyle

TypeScript errors

Common Causes:

  • Incorrect type imports
  • Missing Reanimated types

Solutions:

  • Import types from the hook file
  • Ensure Reanimated types are properly installed
  • Check tsconfig.json configuration
FlatList Integration
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>
);
}
React Navigation Integration
import { useFocusEffect } from '@react-navigation/native';
function NavigationExample() {
const { animatedStyle, onScroll, forceShow } = useHideOnScroll({
height: 60,
hideDirection: 'up',
});
// Always show header when screen comes into focus
useFocusEffect(
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>
);
}