How to hide a search bar on scroll in React Native
March 25, 2021
Following on from my previous post, I want to share another implementation of what I think is a common but less documented UX pattern in React Native. The UX pattern is search bar which animates out of view as the user scrolls down. You may have seen a similar feature in apps like Apple Mail on iOS, and other apps when the search bar is used to filter results in the component below:
I think this is such a useful feature for an app as it maximises the screen real estate on the main body of the app while the user is scrolling down a list, but allows the search bar to be visible in a well prioritised location when the user wants to see it.
Requirements
The end result is that the search bar should be hidden when the app first loads, and the user should be able to tap a search button somewhere on the interface to show the search bar. The main body of the app, under the header and hidden search bar, is a FlatList containing what ever data you like.
When the user presses the search button, the search bar animates in to view by sliding down from under the header, the user can type to filter the list, and then when they begin to scroll down the screen, the search bar disappears.
Setup the environment
The best part about this solution is that there’s no external packages to install, with everything created from the base React Native API.
This example uses functional components with React Hooks as it’s a little easier in situation like this, although if it gets much more complex it might be worth re-doing with a class based setup.
Start by adding a simple page with header and FlatList:
import React, { useState, useRef, useEffect } from "react"
import {
SafeAreaView,
StyleSheet,
View,
Text,
Animated,
TextInput,
FlatList,
} from "react-native"
const DATA = [
{
id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
title: "First Item",
},
{
id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
title: "Second Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d72",
title: "Third Item",
},
]
const App = () => {
const [titleSearchValue, onChangeText] = useState("")
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.whiteText}>{item.title}</Text>
</View>
)
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.header}>
<View>
<Text style={styles.headerText}>Your awesome app</Text>
</View>
</View>
<View style={styles.searchBarWrap}>
<TextInput
onChangeText={(text) => onChangeText(text)}
value={titleSearchValue}
placeholder={"Search..."}
placeholderTextColor={"#fff"}
style={styles.whiteText}
/>
</View>
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
header: {
justifyContent: "center",
alignItems: "center",
borderBottomColor: "#999",
borderBottomWidth: 1,
backgroundColor: "#fff",
position: "relative",
height: 50,
zIndex: 10,
},
headerText: {
color: "#444",
},
searchBarWrap: {
backgroundColor: "#434a5d",
paddingHorizontal: 12,
justifyContent: "center",
height: 45,
},
item: {
backgroundColor: "#716f25",
padding: 20,
marginTop: 4,
marginHorizontal: 4,
},
whiteText: {
color: "#fff",
},
})
export default App
The FlatList setup above is a modified version of the one on the React Native docs - a simple list with a search bar using React useState
to control the input text:
The input field in this post will not be built to actually filter the FlatList as the focus is on how to hide the search bar.
Toggling the search bar on press of a button
Next we want a button in the header (or anywhere you like) to toggle visibility of the search bar so it’s not showing by default, but only when the button is pressed. This can again be accomplished by React useState
:
// ... imports and FlatList data
const App = () => {
const [titleSearchValue, onChangeText] = useState("")
const [toggleSearchBar, setToggleSearchBar] = useState(false)
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.whiteText}>{item.title}</Text>
</View>
)
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.header}>
<View>
<Text style={styles.headerText}>Your awesome app</Text>
</View>
<TouchableOpacity style={styles.searchBtn} onPress={() => setToggleSearchBar(!toggleSearchBar)} > <Text style={styles.headerText}>Search</Text> </TouchableOpacity> </View>
<View style={{ top: toggleSearchBar ? -45 : 0 }}> <View style={styles.searchBarWrap}>
<TextInput
onChangeText={(text) => onChangeText(text)}
value={titleSearchValue}
placeholder={"Search..."}
placeholderTextColor={"#fff"}
style={styles.whiteText}
/>
</View>
</View>
<FlatList data={DATA} renderItem={renderItem} keyExtractor={(item) => item.id} />
</SafeAreaView>
)
}
const styles = StyleSheet.create({
//...
searchBtn: {
position: "absolute",
right: 0,
top: 0,
bottom: 0,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 20,
},
//...
})
export default App
This will do a simple show/hide toggle by moving the search bar up/down by 45px, relative to the header.
Animating the search bar and hiding on scroll
It will look much better if the search bar is animated and it also needs to animate when the user scrolls. This is easy to do with React Native’s built in Animated library:
// ... imports and FlatList data
const App = () => {
const [titleSearchValue, onChangeText] = useState("")
const [toggleSearchBar, setToggleSearchBar] = useState(false)
const searchBarAnim = useRef(new Animated.Value(-45)).current useEffect(() => { if (toggleSearchBar) { Animated.timing(searchBarAnim, { toValue: 0, duration: 300, useNativeDriver: true, }).start() } else { Animated.timing(searchBarAnim, { toValue: -45, duration: 300, useNativeDriver: true, }).start() } }, [toggleSearchBar])
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.whiteText}>{item.title}</Text>
</View>
)
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.header}>
<View>
<Text style={styles.headerText}>Your awesome app</Text>
</View>
<TouchableOpacity
style={styles.searchBtn}
onPress={() => setToggleSearchBar(!toggleSearchBar)}
>
<Text style={styles.headerText}>Search</Text>
</TouchableOpacity>
</View>
<Animated.View style={{ transform: [{ translateY: searchBarAnim }] }}> <View style={styles.searchBarWrap}> <TextInput onChangeText={(text) => onChangeText(text)} value={titleSearchValue} placeholder={"Search..."} placeholderTextColor={"#fff"} style={styles.whiteText} /> </View> </Animated.View> <Animated.FlatList data={DATA} renderItem={renderItem} keyExtractor={(item) => item.id} onScrollBeginDrag={() => setToggleSearchBar(false)} style={{ transform: [{ translateY: searchBarAnim }] }} /> </SafeAreaView>
)
}
// ... styles
export default App
This is where the magic happens! The toggleSearchBar
state we set previously is listened to with useEffect
, so any time that value changes, we can execute some code. Each time the search toggle changes, we use Animated.timing
to start an animation called searchBarAnim
, which is simply a numeric value which we set to animate from -45
to 0
. The searchBarAnim
value is used in the animated View and FlatList to animate the both containers using the style transform
property.
Both containers are animated because it means the search bar doesn’t overlay the FlatList, but instead they both toggle down/up by 45px, giving the illusion that the search bar pushes the list downwards.
The animation process is similar to that of a CSS3 Transform property on the web, only in React Native it uses the native driver so the animations are performed in a performant way, and aren’t laggy with use of the bridge.
The final point in the above snippet is that the search bar is easily hidden on scroll by hooking in to the onScrollBeginDrag
prop of the FlatList:
onScrollBeginDrag={() => setToggleSearchBar(false)}
Here’s the final result:
I’ve added the code for this to a separate repo which can be viewed here.
Thanks for reading!
Senior Engineer at Haven