Animated Header
A react native header component that smoothly animates into and out of view based on scroll direction.
react-nativereanimatedheaderanimationui-componentscrollflash-listsafe-area
Step 1: Install Dependencies
npx expo install @shopify/flash-list react-native-safe-area-context @expo/vector-icons/Ionicons
Step 2: Create the Header Component
Create a new file (e.g., src/components/Header.tsx
) and copy the following code:
Header.tsx
import React from "react";
import { View, Text, StyleSheet, Image, TouchableOpacity } from "react-native";
import Ionicons from "@expo/vector-icons/Ionicons";
import Animated, {
Extrapolation,
interpolate,
SharedValue,
useAnimatedStyle,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useAppColors } from "@/hooks/useAppColors";
export const HEADER_HEIGHT = 60;
type HeaderProps = {
headerShown: SharedValue<number>;
};
export function Header({ headerShown }: HeaderProps) {
const colors = useAppColors();
const insets = useSafeAreaInsets();
const headerAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY: interpolate(
headerShown.value,
[0, 1],
[-HEADER_HEIGHT, 0],
Extrapolation.CLAMP
),
},
],
};
});
const contentAnimatedStyle = useAnimatedStyle(() => {
return {
opacity: interpolate(
headerShown.value,
[0, 1],
[0, 1],
Extrapolation.CLAMP
),
};
});
return (
<Animated.View
style={[
styles.header,
headerAnimatedStyle,
{ top: insets.top, backgroundColor: colors.Neutral0 },
]}
>
<Animated.View style={[styles.headerContainer, contentAnimatedStyle]}>
{/* Left Side: Profile Image and Username */}
<View style={styles.profileSection}>
<Image
source={{ uri: "https://picsum.photos/100" }}
style={styles.profileImage}
/>
<View style={styles.textContainer}>
<Text style={[styles.usernameText, { color: colors.Neutral500 }]}>
{"Guest"}
</Text>
</View>
</View>
{/* Right Side: Icons */}
<View style={styles.iconContainer}>
<TouchableOpacity style={styles.iconButton}>
<Ionicons
name="search-outline"
size={24}
color={colors.Neutral500}
/>
</TouchableOpacity>
<TouchableOpacity style={styles.iconButton}>
<Ionicons
name="notifications-outline"
size={24}
color={colors.Neutral500}
/>
</TouchableOpacity>
</View>
</Animated.View>
</Animated.View>
);
}
const styles = StyleSheet.create({
header: {
position: "absolute",
top: 0,
left: 0,
right: 0,
zIndex: 1,
shadowRadius: 3,
// elevation: 4,
},
headerContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 10,
paddingHorizontal: 16,
zIndex: 10,
width: "100%",
height: HEADER_HEIGHT,
},
profileSection: {
flexDirection: "row",
alignItems: "center",
},
profileImage: {
width: 40,
height: 40,
borderRadius: 20,
marginRight: 8,
},
textContainer: {
alignItems: "flex-start",
},
usernameText: {
fontSize: 16,
fontWeight: "600",
},
buyerText: {
fontSize: 12,
fontWeight: "400",
},
iconContainer: {
flexDirection: "row",
// alignItems: "center",
},
iconButton: {
marginLeft: 16,
},
});
Step 3: Usage with Scroll Logic
Create a screen component (e.g., src/screens/DemoScreen.tsx
) where you'll use the Header
and implement the scroll animation logic.
DemoScreen.tsx
import React from "react";
import { View, StyleSheet, Dimensions } from "react-native";
import Animated, { useSharedValue, useAnimatedScrollHandler, withSpring } from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";
import { Header, HEADER_HEIGHT } from "./Header";
import { FlashList, FlashListProps } from "@shopify/flash-list";
import { useAppColors } from "@/hooks/useAppColors";
const defaultData = Array.from({ length: 100 }, (_, i) => ({ id: `item-${i}` }));
const { width } = Dimensions.get("window");
const NUM_COLUMNS = 2;
const ITEM_MARGIN = 8;
const ITEM_WIDTH = width / NUM_COLUMNS - (ITEM_MARGIN * (NUM_COLUMNS + 1)) / NUM_COLUMNS;
export default function AnimatedHeaderDemo() {
const scrollY = useSharedValue(0);
const lastScrollY = useSharedValue(0);
const headerShown = useSharedValue(1);
const colors = useAppColors();
const AnimatedFlashList = Animated.createAnimatedComponent<FlashListProps<any>>(FlashList);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
const currentScrollY = event.contentOffset.y;
// Only process scroll if we're not in the bounce area (y >= 0)
if (currentScrollY >= 0) {
const dy = currentScrollY - lastScrollY.value;
// Detect if we're actively scrolling or if it's bounce movement
if (Math.abs(dy) > 0.5) {
// Using a smaller divisor for more responsive movement
const newValue = headerShown.value + -dy / 50;
// Clamp the value with better boundary handling
headerShown.value = withSpring(Math.min(Math.max(newValue, 0), 1), {
damping: 15,
stiffness: 200,
});
}
}
lastScrollY.value = currentScrollY;
scrollY.value = currentScrollY;
},
});
const renderItem = ({ item }: { item: { id: string } }) => {
return (
<View key={item.id} style={[styles.cardItem, { backgroundColor: colors.Neutral50 }]}>
{/* Skeleton Placeholders */}
<View style={[styles.skeletonImage, { backgroundColor: colors.Neutral90 }]} />
<View style={[styles.skeletonLine, { width: "100%", backgroundColor: colors.Neutral90 }]} />
<View style={[styles.skeletonLine, { width: "80%", backgroundColor: colors.Neutral90 }]} />
<View style={[styles.skeletonLine, { width: "60%", backgroundColor: colors.Neutral90 }]} />
</View>
);
};
return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.Neutral0 }]}>
<Header headerShown={headerShown} />
<View style={styles.scrollView}>
<AnimatedFlashList
contentContainerStyle={{
paddingTop: HEADER_HEIGHT + ITEM_MARGIN,
paddingHorizontal: ITEM_MARGIN / 2,
}}
numColumns={NUM_COLUMNS}
data={defaultData}
showsVerticalScrollIndicator={false}
onScroll={scrollHandler}
estimatedItemSize={150}
renderItem={renderItem}
scrollEventThrottle={16}
/>
</View>
</SafeAreaView>
);
}
// Styles
const styles = StyleSheet.create({
container: {
flex: 1,
},
cardItem: {
width: ITEM_WIDTH,
margin: ITEM_MARGIN / 2,
borderRadius: 8,
padding: 12,
},
skeletonImage: {
width: "100%",
height: ITEM_WIDTH * 0.6,
borderRadius: 6,
marginBottom: 10,
},
skeletonLine: {
height: 12,
borderRadius: 4,
marginBottom: 8,
},
scrollView: {
flex: 1,
},
contentContainer: {
paddingTop: 50,
paddingBottom: 50,
},
item: {
padding: 20,
borderBottomWidth: 1,
},
text: {
fontSize: 16,
fontWeight: "400",
},
});
Props
Prop | Type | Default | Required | Description |
---|---|---|---|---|
headerShown | SharedValue<number> | Yes | A Reanimated SharedValue ranging from 0 (hidden) to 1 (shown) to control visibility. |
Customization
- Animation: Tweak the
withSpring
configuration (damping, stiffness) in thescrollHandler
for different animation feels. You could also usewithTiming
for a duration-based animation. Modify the interpolation ranges or extrapolation inHeader.tsx
for different visual effects. - Scroll Logic: Adjust the thresholds (
dy > 5
,dy < -5
) in thescrollHandler
to change sensitivity to scroll direction changes.