Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Scroll-Responsive Animated Header Bar with Expo Router
.
.

Posted on

     

Scroll-Responsive Animated Header Bar with Expo Router

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

Demo of animated header

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
Enter fullscreen modeExit fullscreen mode

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,},});
Enter fullscreen modeExit fullscreen mode

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;
Enter fullscreen modeExit fullscreen mode

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",});
Enter fullscreen modeExit fullscreen mode

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,},]}/>
Enter fullscreen modeExit fullscreen mode

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;
Enter fullscreen modeExit fullscreen mode

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});
Enter fullscreen modeExit fullscreen mode

⚠️ 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",},});
Enter fullscreen modeExit fullscreen mode

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
uicraft_by_pratik profile image
Pratik Tamhane
UICraft by Pratik 🚀 brings a unique approach to modern UI. I blend human intuition🤖 with cutting-edge tech to craft innovative digital experiences. As a multidisciplinary designer & developer✨
  • Email
  • Location
    India, Maharashrta
  • Education
    BE.Civil in SPPU Pune University, Ui/Ux Designer in UiUx Design School Pune
  • Pronouns
    Pratik
  • Work
    Ui/Ux Designer
  • Joined

Nice🚀

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Joined

More from.

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp