Avatar

Salut, I'm Julia.

Creating a Full Screen Pressable Overlay From a Bottom Tab Navigator in React Native

#react-native

4 min read

I came across a React Native issue last week, which initially got me rather confused as I thought it had to do with z-index positioning on Android. Turns out, there's an identified RN issue to do specifically, with how Pressable child components are rendered on Android. Scroll to the bottom of this page if you're interested in more specifics.

To provide more context on my specific use case, I decided to try my hand at some diagrams. It's not the best, but hopefully does the job. ๐Ÿ˜› If anyone has tips on how to export hand-drawn diagrams on an iPad to a Mac laptop, let me know.

The problem

Picture a mobile screen. At the bottom of the screen, I've got a React Navigation bottom tab navigator.

Mobile screen with tab navigator

What I wanted was to have a darker overlay appear over the whole screen which would disappear upon being pressed i.e. a Pressable overlay. Because of some other side effects, the trigger for showing / hiding the overlay needed to be done from the tab navigator.

Pressable overlay

To do this, I used the <Tab.Screen> options prop to pass in a custom tabBarIcon, that includes not just the icon for the tab, but also a conditional Pressable overlay which looks something like this... (note that I set the height and width of the overlay to take the full screen using the useWindowDimensions hook from react-native).

export default function TabNavigator() {
  const windowDimensions = useWindowDimensions()
  const [showOverlay, setShowOverlay] = React.useState < boolean > false

  const tabScreenOptions = {
    // other options
    tabBarIcon: ({ focused, color }: { focused: boolean, color: string }) => {
      return (
        <>
          // tab icon component
          {showOverlay ? (
            <Pressable
              onPress={closeOverlay}
              style={[styles.overlay, { height: windowDimensions.height, width: windowDimensions.width }]}
            />
          ) : null}
        </>
      )
    },
  }

  return (
    <Tab.Navigator>
      // Other tab screens
      <Tab.Screen component={TabScreen} name="Tab name" options={tabScreenOptions} />
    </Tab.Navigator>
  )
}

const styles = StyleSheet.create({
  overlay: {
    backgroundColor: 'black',
    flex: 1,
    opacity: 0.5,
    position: 'absolute',
    zIndex: 1,
  },
})

When testing on iOS, all works as expected. Upon the showOverlay state variable being set to true, the full screen overlay appears and is pressable. ๐ŸŽ‰

iOS pressable area

Android does not work as expected however, because Android only allows the overlapping area of the child (i.e. the overlay) and parent (tab navigator) to be pressable. In my case, because the tab navigator is smaller than the full screen overlay, I'm stuck. In order to mimic the behaviour seen in iOS, I'd need to make the tab navigator height and width to be that of the entire screen... which isn't a viable option in my case.

Android pressable area

A potential solution

So what did I do? I decided to go down a slightly convoluted path in order to get more flexibility by using React context. As the tab navigator acts as the parent for all the screens and any child components present within the screens, I could set up a context provider at the tab navigator level and then put in place context consumers at whatever component I needed down the hierachy.

export function TabNavigator() {
  const [showOverlay, setShowOverlay] = React.useState < boolean > false
  export const OverlayContext = React.createContext({ closeOverlay: () => {}, showOverlay: false })

  // const tabScreenOptions same as before

  const overlayContext = {
    closeOverlay,
    showOverlay,
  }

  const closeOverlay = React.useCallback(async () => {
    setShowOverlay(false)
    // do some other things
  }, [])

  return (
    <OverlayContext.Provider value={overlayContext}>
      <Tab.Navigator>
        // Other tab screens
        <Tab.Screen component={TabScreen} name="Tab name" options={tabScreenOptions} />
      </Tab.Navigator>
    </OverlayContext.Provider>
  )
}

To set up a consumer, in any child component of the TabNavigator, I used the useContext hook.

export function ChildComponent() {
  const overlayContext = React.useContext(OverlayContext)

  return (
    <>
      {Platform.OS === 'android' && overlayContext.showOverlay ? (
        <Pressable
          onPress={overlayContext.closeOverlay}
          style={[styles.overlay, { height: windowDimensions.height, width: windowDimensions.width }]}
        />
      ) : null}
      // the child component
    </>
  )
}

To be specific, I used the Platform module from react-native to check that this only shows up for Android operating systems. The styles.overlay styling is the same as what I had previously.

ยฉ 2016-2024 Julia Tan ยท Powered by Next JS.