Jay Gould

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:

Hide search bar on scroll in React Native

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:

Hide search bar screenshot

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:

Hide search bar animated gif

I’ve added the code for this to a separate repo which can be viewed here.

Thanks for reading!


Senior Engineer at Haven

© Jay Gould 2023, Built with love and tequila.