Skip to main content
Overlays are persistent components that render above all screens in the stack and can animate in response to navigation changes. Perfect for tab bars, player controls, or custom navigation UI.

Basic Usage

import { useAnimatedStyle, interpolate } from "react-native-reanimated";
import Animated from "react-native-reanimated";

const TabBar = ({ progress }) => {
  const style = useAnimatedStyle(() => ({
    transform: [{ translateY: interpolate(progress.value, [0, 1], [100, 0]) }],
  }));

  return (
    <Animated.View style={[styles.tabBar, style]}>
      {/* Tab bar content */}
    </Animated.View>
  );
};

<Stack.Screen
  name="Home"
  options={{
    overlay: TabBar,
    overlayShown: true,
  }}
/>

Configuration

overlay

overlay
(props: OverlayProps) => React.ReactNode
Function that returns a React Element to display as an overlay. The overlay receives animation values and navigation state as props.
options={{
  overlay: (props) => <CustomOverlay {...props} />,
}}

overlayShown

overlayShown
boolean
default:"true"
Whether to show the overlay. The overlay is shown by default when overlay is provided. Set to false to hide the overlay for specific screens.
// Show overlay on home
<Stack.Screen
  name="Home"
  options={{
    overlay: TabBar,
    overlayShown: true,
  }}
/>

// Hide overlay on detail
<Stack.Screen
  name="Detail"
  options={{
    overlay: TabBar,
    overlayShown: false,
  }}
/>

Overlay Props

Overlay components receive the following props:

focusedRoute

focusedRoute
Route<string>
Route object of the currently focused screen in the stack.
const TabBar = ({ focusedRoute }) => {
  const isHomeActive = focusedRoute.name === "Home";
  // Highlight appropriate tab
};

focusedIndex

focusedIndex
number
Index of the focused route in the stack (0-based).
const TabBar = ({ focusedIndex }) => {
  console.log(`Screen ${focusedIndex} is active`);
};

routes

routes
Route<string>[]
All routes currently in the stack.
const TabBar = ({ routes }) => {
  const stackDepth = routes.length;
  // Adjust UI based on stack depth
};

progress

progress
DerivedValue<number>
Stack progress relative to the overlay’s position. Equivalent to useScreenAnimation().stackProgress.Use this for animating the overlay based on navigation state.
const TabBar = ({ progress }) => {
  const style = useAnimatedStyle(() => ({
    opacity: interpolate(progress.value, [0, 1], [0, 1]),
    transform: [
      { translateY: interpolate(progress.value, [0, 1], [100, 0]) }
    ],
  }));

  return <Animated.View style={style}>{/* Content */}</Animated.View>;
};

meta

meta
Record<string, unknown>
Custom metadata from the focused screen’s options.
// In screen options
<Stack.Screen
  name="Home"
  options={{
    meta: { showTabBar: true },
    overlay: TabBar,
  }}
/>

// In overlay component
const TabBar = ({ meta }) => {
  if (!meta?.showTabBar) return null;
  return <View>{/* Tab bar */}</View>;
};
navigation
Navigation
Navigation prop for the overlay. Type depends on which stack you’re using (Blank, Native, or Component).
const TabBar = ({ navigation }) => {
  return (
    <Button
      title="Go to Settings"
      onPress={() => navigation.navigate("Settings")}
    />
  );
};

options

options
ScreenTransitionConfig
Screen options for the currently focused screen.
const TabBar = ({ options }) => {
  const hasGestures = options.gestureEnabled;
  // Adjust behavior based on screen config
};

Examples

Animated Tab Bar

import { useAnimatedStyle, interpolate } from "react-native-reanimated";
import Animated from "react-native-reanimated";
import { StyleSheet } from "react-native";

const TabBar = ({ focusedRoute, progress, navigation }) => {
  const style = useAnimatedStyle(() => ({
    transform: [
      { translateY: interpolate(progress.value, [0, 1], [100, 0]) },
    ],
    opacity: interpolate(progress.value, [0, 1], [0, 1]),
  }));

  return (
    <Animated.View style={[styles.tabBar, style]}>
      <TabButton
        title="Home"
        active={focusedRoute.name === "Home"}
        onPress={() => navigation.navigate("Home")}
      />
      <TabButton
        title="Search"
        active={focusedRoute.name === "Search"}
        onPress={() => navigation.navigate("Search")}
      />
      <TabButton
        title="Profile"
        active={focusedRoute.name === "Profile"}
        onPress={() => navigation.navigate("Profile")}
      />
    </Animated.View>
  );
};

const styles = StyleSheet.create({
  tabBar: {
    position: "absolute",
    bottom: 0,
    left: 0,
    right: 0,
    height: 60,
    flexDirection: "row",
    backgroundColor: "white",
    borderTopWidth: 1,
    borderTopColor: "#e0e0e0",
  },
});

// Apply to screens
<Stack.Screen
  name="Home"
  options={{
    overlay: TabBar,
    overlayShown: true,
  }}
/>
<Stack.Screen
  name="Search"
  options={{
    overlay: TabBar,
    overlayShown: true,
  }}
/>
<Stack.Screen
  name="Detail"
  options={{
    overlay: TabBar,
    overlayShown: false, // Hide on detail screen
  }}
/>

Music Player Overlay

import { useAnimatedStyle, interpolate } from "react-native-reanimated";
import Animated from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";

const MiniPlayer = ({ progress, meta }) => {
  const insets = useSafeAreaInsets();
  
  // Hide player on certain screens
  if (meta?.hidePlayer) return null;

  const style = useAnimatedStyle(() => ({
    transform: [
      { translateY: interpolate(progress.value, [0, 1], [100, 0]) },
    ],
  }));

  return (
    <Animated.View
      style={[
        {
          position: "absolute",
          bottom: insets.bottom + 60, // Above tab bar
          left: 0,
          right: 0,
          height: 60,
          backgroundColor: "#1DB954",
          flexDirection: "row",
          alignItems: "center",
          paddingHorizontal: 16,
        },
        style,
      ]}
    >
      <Image source={albumArt} style={{ width: 40, height: 40 }} />
      <View style={{ flex: 1, marginLeft: 12 }}>
        <Text style={{ color: "white", fontWeight: "bold" }}>Song Title</Text>
        <Text style={{ color: "#b3b3b3" }}>Artist Name</Text>
      </View>
      <PlayPauseButton />
    </Animated.View>
  );
};

// Apply to screens
<Stack.Screen
  name="Home"
  options={{
    overlay: MiniPlayer,
    overlayShown: true,
  }}
/>
<Stack.Screen
  name="Player"
  options={{
    overlay: MiniPlayer,
    overlayShown: false, // Hide on full player
    meta: { hidePlayer: true },
  }}
/>

Conditional Overlay with Meta

const ConditionalOverlay = ({ meta, progress }) => {
  // Different overlays for different screen types
  if (meta?.overlayType === "tabBar") {
    return <TabBar progress={progress} />;
  }
  
  if (meta?.overlayType === "toolbar") {
    return <Toolbar progress={progress} />;
  }
  
  return null;
};

// Configure per screen
<Stack.Screen
  name="Home"
  options={{
    overlay: ConditionalOverlay,
    meta: { overlayType: "tabBar" },
  }}
/>
<Stack.Screen
  name="Editor"
  options={{
    overlay: ConditionalOverlay,
    meta: { overlayType: "toolbar" },
  }}
/>
<Stack.Screen
  name="Detail"
  options={{
    overlay: ConditionalOverlay,
    // No meta.overlayType - renders nothing
  }}
/>

Stack Depth Indicator

const StackDepth = ({ routes, focusedIndex }) => {
  return (
    <View
      style={{
        position: "absolute",
        top: 50,
        right: 20,
        backgroundColor: "rgba(0,0,0,0.7)",
        padding: 8,
        borderRadius: 4,
      }}
    >
      <Text style={{ color: "white" }}>
        {focusedIndex + 1} / {routes.length}
      </Text>
    </View>
  );
};

Using useScreenAnimation in Overlays

You can also use useScreenAnimation() inside overlay components for more complex animations:
import { useScreenAnimation } from "react-native-screen-transitions";
import { useAnimatedStyle, interpolate } from "react-native-reanimated";
import Animated from "react-native-reanimated";

const AdvancedOverlay = () => {
  const animation = useScreenAnimation();

  const style = useAnimatedStyle(() => {
    const progress = animation.value.current.progress;
    const stackProgress = animation.stackProgress.value;

    return {
      opacity: interpolate(stackProgress, [0, 1, 2], [0, 1, 0.5]),
      transform: [
        {
          scale: interpolate(progress, [0, 1], [0.9, 1]),
        },
        {
          translateY: interpolate(stackProgress, [0, 1], [50, 0]),
        },
      ],
    };
  });

  return (
    <Animated.View style={[styles.overlay, style]}>
      {/* Overlay content */}
    </Animated.View>
  );
};

Best Practices

Use progress for Animations

The progress prop is optimized for overlay animations. Use it with useAnimatedStyle to create smooth, performant animations.

Control Visibility with overlayShown

Instead of conditionally rendering overlays, use overlayShown to control visibility. This maintains overlay state across screen transitions.

Pass Data with meta

Use the meta option to pass custom data to overlays. This is more type-safe and maintainable than global state for overlay configuration.

Position Absolutely

Overlays should use absolute positioning to float above screen content. Use safe area insets for proper spacing on notched devices.

Common Patterns

Hiding Overlay on Specific Screens

// Method 1: Using overlayShown
<Stack.Screen
  name="Fullscreen"
  options={{
    overlay: TabBar,
    overlayShown: false,
  }}
/>

// Method 2: Using meta
<Stack.Screen
  name="Fullscreen"
  options={{
    overlay: ({ meta }) => {
      if (meta?.hideOverlay) return null;
      return <TabBar />;
    },
    meta: { hideOverlay: true },
  }}
/>

Different Overlays for Different Screens

const HomeOverlay = (props) => <TabBar {...props} />;
const EditorOverlay = (props) => <Toolbar {...props} />;

<Stack.Screen
  name="Home"
  options={{ overlay: HomeOverlay }}
/>
<Stack.Screen
  name="Editor"
  options={{ overlay: EditorOverlay }}
/>

Animating Based on Route

const SmartOverlay = ({ focusedRoute, progress }) => {
  const style = useAnimatedStyle(() => {
    // Different animations for different routes
    const shouldHide = focusedRoute.name === "Detail";
    
    return {
      transform: [
        {
          translateY: interpolate(
            progress.value,
            [0, 1],
            [shouldHide ? 100 : 0, 0]
          ),
        },
      ],
    };
  });

  return <Animated.View style={style}>{/* Content */}</Animated.View>;
};

TypeScript

import type { OverlayProps } from "react-native-screen-transitions";
import type { BlankStackNavigationProp } from "react-native-screen-transitions/blank-stack";

// Type your overlay component
const TabBar = (props: OverlayProps<BlankStackNavigationProp>) => {
  const { focusedRoute, progress, navigation } = props;
  // Fully typed props
};

// With custom meta
interface CustomMeta {
  showTabBar: boolean;
  theme: "light" | "dark";
}

const TabBar = (props: OverlayProps) => {
  const meta = props.meta as CustomMeta | undefined;
  // Type-safe meta access
};