Skip to main content
Overlays are persistent UI elements that animate with the navigation stack. Common examples are tab bars, floating action buttons, and persistent headers.

Overlay Pattern

An overlay is a component that receives navigation state and progress, then renders based on which screen is focused:
import Animated, { interpolate } from "react-native-reanimated";
import { View, Text } from "react-native";

function TabBarOverlay({
  focusedIndex,
  progress,
  focusedRoute,
  routes,
}) {
  "worklet";

  return (
    <Animated.View
      style={{
        position: "absolute",
        bottom: 0,
        left: 0,
        right: 0,
        height: 50,
        backgroundColor: "#fff",
        flexDirection: "row",
        opacity: interpolate(progress, [0, 1], [0.5, 1]),
      }}
    >
      {routes.map((route, index) => (
        <View key={route.key} style={{ flex: 1 }}>
          <Text
            style={{
              color: focusedIndex === index ? "#007AFF" : "#999",
            }}
          >
            {route.name}
          </Text>
        </View>
      ))}
    </Animated.View>
  );
}

Overlay Props

Overlays receive these props from the stack navigator:
PropTypeDescription
focusedRouteRouteThe currently focused route
focusedIndexnumberIndex of focused route (0-based)
routesRoute[]All routes in the stack
progressAnimated ValueStack animation progress (0-1)
navigationNavigationNavigator instance
metaobjectCustom metadata

Using Overlays in Blank Stack

Pass an overlay component to Navigator:
import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";

const Stack = createBlankStackNavigator();

function MyTabBar({ focusedIndex, routes }) {
  return (
    <View style={{ position: "absolute", bottom: 0, left: 0, right: 0 }}>
      {routes.map((route, index) => (
        <TouchableOpacity key={route.key}>
          <Text style={{ color: focusedIndex === index ? "#007AFF" : "#999" }}>
            {route.name}
          </Text>
        </TouchableOpacity>
      ))}
    </View>
  );
}

<Stack.Navigator
  screenOptions={{ headerShown: false }}
  overlay={MyTabBar}
>
  <Stack.Screen name="Home" component={HomeScreen} />
  <Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>

Example: Animated Tab Bar

A tab bar that fades out when a detail screen opens:
import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
import Transition from "react-native-screen-transitions";
import Animated, { interpolate } from "react-native-reanimated";
import { View, Text, TouchableOpacity } from "react-native";

const Stack = createBlankStackNavigator();

function TabBar({ focusedRoute, focusedIndex, routes, progress, navigation }) {
  "worklet";

  // Hide tab bar when detail screen is active
  const opacity = interpolate(
    progress,
    [0, 0.5, 1],
    [1, 0.5, 0]
  );

  return (
    <Animated.View
      style={{
        position: "absolute",
        bottom: 0,
        left: 0,
        right: 0,
        height: 50,
        backgroundColor: "#fff",
        flexDirection: "row",
        borderTopWidth: 1,
        borderTopColor: "#eee",
        opacity,
      }}
    >
      {routes.map((route, index) => (
        <TouchableOpacity
          key={route.key}
          style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
          onPress={() => navigation.navigate(route.name)}
        >
          <Text
            style={{
              color: focusedIndex === index ? "#007AFF" : "#999",
              fontSize: 12,
            }}
          >
            {route.name}
          </Text>
        </TouchableOpacity>
      ))}
    </Animated.View>
  );
}

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, justifyContent: "center", paddingBottom: 50 }}>
      <TouchableOpacity onPress={() => navigation.navigate("Detail")}>
        <Text>Open Detail</Text>
      </TouchableOpacity>
    </View>
  );
}

function DetailScreen() {
  return <View style={{ flex: 1, backgroundColor: "#fff" }} />;
}

export default function App() {
  return (
    <Stack.Navigator
      screenOptions={{ headerShown: false }}
      overlay={TabBar}
    >
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen
        name="Detail"
        component={DetailScreen}
        options={{
          ...Transition.Presets.SlideFromBottom(),
          gestureEnabled: true,
          gestureDirection: "vertical",
        }}
      />
    </Stack.Navigator>
  );
}

Example: Floating Action Button

A FAB that changes based on the focused screen:
function FloatingButton({ focusedRoute, focusedIndex, progress, navigation }) {
  "worklet";

  const isHome = focusedRoute?.name === "Home";
  const icon = isHome ? "+" : "←";
  const onPress = isHome
    ? () => navigation.navigate("Add")
    : () => navigation.goBack();

  return (
    <Animated.View
      style={{
        position: "absolute",
        bottom: 24,
        right: 24,
        width: 56,
        height: 56,
        borderRadius: 28,
        backgroundColor: "#007AFF",
        justifyContent: "center",
        alignItems: "center",
        opacity: interpolate(progress, [0, 1], [0.5, 1]),
      }}
    >
      <TouchableOpacity onPress={onPress}>
        <Text style={{ color: "#fff", fontSize: 24 }}>{icon}</Text>
      </TouchableOpacity>
    </Animated.View>
  );
}

Example: Header Overlay

A persistent header that changes based on the focused screen:
function HeaderOverlay({ focusedRoute, progress }) {
  "worklet";

  const title = focusedRoute?.name || "App";
  const backgroundColor = interpolate(
    progress,
    [0, 0.5, 1],
    ["#fff", "#f5f5f5", "#f0f0f0"]
  );

  return (
    <Animated.View
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        right: 0,
        height: 56,
        backgroundColor,
        justifyContent: "center",
        paddingHorizontal: 16,
        borderBottomWidth: 1,
        borderBottomColor: "#eee",
      }}
    >
      <Text style={{ fontSize: 18, fontWeight: "bold" }}>{title}</Text>
    </Animated.View>
  );
}

Overlay Visibility Control

Control when overlays are visible:
<Stack.Navigator
  screenOptions={{ headerShown: false }}
  overlay={MyOverlay}
  overlayShown={true}  // or false to hide
>
  {/* screens */}
</Stack.Navigator>
The overlayShown prop determines whether the overlay is rendered at all.

Performance Notes

Overlays receive animated values, so they re-render on every progress update. Keep them simple:
  • ✅ Use Animated.View, Animated.Text
  • ✅ Do simple interpolations
  • ❌ Avoid expensive computations
  • ❌ Avoid heavy rendering operations

Next Steps