
A common UI pattern you'll see in mobile apps is the "native" header dynamically transitioning elements in and out or animating colors as you scroll up and down. Using Expo Router'sStack
component, we can create a reusable component that abstracts much of the logic while maintaining flexibility through prop customisation.
We'll be creating a component calledAnimatedHeaderScreen
which you can quickly wrap around screens to add this functionality. While customisation will depend on specific needs, we'll be animating optional left/right icons and changing the background color, along with applying small details like a border.
What we'll be building
Prerequisites
This tutorial assumes you're using Expo Router in your project, as we'll be utilising components likeStack.Screen
. If you want to start with a fresh install, you can use the following command to create a new TypeScript project with Expo:
npx create-expo-app@latest
Diving into the implementation
importReact,{useRef,ReactNode,useCallback}from"react";import{View,Animated,ScrollView,StyleSheet,TouchableOpacity,}from"react-native";import{Stack}from"expo-router";import{Ionicons}from"@expo/vector-icons";import{useSafeAreaInsets}from"react-native-safe-area-context";typeAnimatedHeaderScreenProps={children:ReactNode;title?:string;leftIcon?:{name:keyoftypeofIonicons.glyphMap;onPress:()=>void;};rightIcon?:{name:keyoftypeofIonicons.glyphMap;onPress:()=>void;};};constcolors={background:"#000000",backgroundScrolled:"#1C1C1D",headerBorder:"#2C2C2E",borderColor:"#3A3A3C",text:"#FFFFFF",tint:"#4A90E2",};exportdefaultfunctionAnimatedHeaderScreen({title,children,leftIcon,rightIcon,}:AnimatedHeaderScreenProps){constscrollY=useRef(newAnimated.Value(0)).current;constinsets=useSafeAreaInsets();constheaderBackgroundColor=scrollY.interpolate({inputRange:[0,50],outputRange:[colors.background,colors.backgroundScrolled],extrapolate:"clamp",});consthandleScroll=Animated.event([{nativeEvent:{contentOffset:{y:scrollY}}}],{useNativeDriver:false});constheaderBorderWidth=scrollY.interpolate({inputRange:[0,50],outputRange:[0,StyleSheet.hairlineWidth],extrapolate:"clamp",});constrightIconOpacity=rightIcon?scrollY.interpolate({inputRange:[30,50],outputRange:[0,1],extrapolate:"clamp",}):0;constrightIconTranslateY=rightIcon?scrollY.interpolate({inputRange:[30,50],outputRange:[10,0],extrapolate:"clamp",}):0;return(<><Stack.Screenoptions={{headerShown:true,headerTitleAlign:"center",headerTitle:title,headerLeft:leftIcon?()=>(<Animated.Viewstyle={{opacity:rightIconOpacity,transform:[{translateY:rightIconTranslateY}],}}><TouchableOpacityonPress={leftIcon.onPress}><Ioniconsname={leftIcon.name}size={24}color={colors.tint}style={styles.leftIcon}/></TouchableOpacity></Animated.View>):undefined,headerRight:rightIcon?()=>(<Animated.Viewstyle={{opacity:rightIconOpacity,transform:[{translateY:rightIconTranslateY}],}}><TouchableOpacityonPress={rightIcon.onPress}><Ioniconsname={rightIcon.name}size={24}color={colors.tint}style={styles.rightIcon}/></TouchableOpacity></Animated.View>):undefined,headerBackground:()=>(<Animated.Viewstyle={[StyleSheet.absoluteFill,styles.headerBackground,{backgroundColor:headerBackgroundColor,borderBottomColor:colors.borderColor,borderBottomWidth:headerBorderWidth,},]}/>),}}/><ScrollViewstyle={styles.scrollView}contentContainerStyle={[styles.scrollViewContent,{paddingBottom:insets.bottom},]}onScroll={handleScroll}scrollEventThrottle={16}><Viewstyle={styles.content}>{children}</View></ScrollView></>);}conststyles=StyleSheet.create({scrollView:{flex:1,},scrollViewContent:{flexGrow:1,},content:{flex:1,paddingHorizontal:8,paddingTop:8,},headerBackground:{borderBottomWidth:0,},leftIcon:{marginLeft:16,},rightIcon:{marginRight:16,},});
How It Works
Tracking Scroll Position
We use anAnimated.Value
to keep tabs on how far the user has scrolled:
constscrollY=useRef(newAnimated.Value(0)).current;
This value updates as the user scrolls, which we'll use to drive our animations.
Smooth Transitions with Interpolation
We useinterpolate
to map the scroll position to different style properties. For example:
constheaderBackgroundColor=scrollY.interpolate({inputRange:[0,50],outputRange:[colors.background,colors.backgroundScrolled],extrapolate:"clamp",});
This creates a smooth color change for the header background as you scroll from 0 to 50 pixels. Theclamp
part just makes sure the color doesn't keep changing beyond what we've set.
Applying Animated Styles
We use these interpolated values in our components withAnimated.View
and inline styles:
<Animated.Viewstyle={[StyleSheet.absoluteFill,styles.headerBackground,{backgroundColor:headerBackgroundColor,borderBottomColor:colors.borderColor,borderBottomWidth:headerBorderWidth,},]}/>
This lets the header update its look based on how far you've scrolled.
Animating Optional Elements
For things like icons, we only apply animations if they're actually there:
constrightIconOpacity=rightIcon?scrollY.interpolate({inputRange:[30,50],outputRange:[0,1],extrapolate:"clamp",}):0;
This way, icons fade in smoothly, but only if you've included them as props.
Handling Scroll Events
We useAnimated.event
to connect scroll events directly to ourscrollY
value:
consthandleScroll=Animated.event([{nativeEvent:{contentOffset:{y:scrollY}}}],{useNativeDriver:false});
⚠️ Note: Make sure you haveuseNativeDriver
set tofalse
or you'll encounter the error: "_this.props.onScroll is not a function (it is Object)". This occurs because the native driver can only handle a subset of styles that can be animated on the native side. We're animating non-compatible styles likebackgroundColor
, which requires JavaScript based animations.
Usage
To use theAnimatedHeaderScreen
, simply wrap your screen content with it:
import{Alert,StyleSheet,Text,View}from"react-native";importAnimatedHeaderScreenfrom"@/components/AnimatedHeaderScreen";exportdefaultfunctionHomeScreen(){return(<AnimatedHeaderScreentitle="Lorem"rightIcon={{name:"search",onPress:()=>Alert.alert("Handle search here..."),}}>{/* // Mock cards to fill out the screen... */}{Array.from({length:20},(_,index)=>index+1).map((item)=>(<Viewstyle={[styles.card,{backgroundColor:item%2===0?"#4A90E2":"#67B8E3"},]}key={item}><Textstyle={styles.text}>{item}</Text></View>))}</AnimatedHeaderScreen>);}conststyles=StyleSheet.create({card:{height:80,elevation:6,marginTop:16,shadowRadius:4,borderRadius:12,shadowOpacity:0.1,marginHorizontal:8,alignItems:"center",justifyContent:"center",shadowOffset:{width:0,height:3},},text:{color:"#FFF",fontSize:16,fontWeight:"bold",},});
That's it! You've now got a solid foundation for an animated header in your Expo Router app. Feel free to tweak the animations, add more interactive elements, or adjust the styling to fit your app's needs.
Top comments(1)

- Email
- LocationIndia, Maharashrta
- EducationBE.Civil in SPPU Pune University, Ui/Ux Designer in UiUx Design School Pune
- PronounsPratik
- WorkUi/Ux Designer
- Joined
Nice🚀
For further actions, you may consider blocking this person and/orreporting abuse