diff --git a/native/chat/reaction-selection-popover.react.js b/native/chat/reaction-selection-popover.react.js
--- a/native/chat/reaction-selection-popover.react.js
+++ b/native/chat/reaction-selection-popover.react.js
@@ -52,6 +52,7 @@
     'ReactionSelectionPopover should have OverlayContext',
   );
   const { position } = overlayContext;
+  invariant(position, 'position should be defined in ReactionSelectionPopover');
 
   const dimensions = useSelector(state => state.dimensions);
 
diff --git a/native/components/full-screen-view-modal.react.js b/native/components/full-screen-view-modal.react.js
--- a/native/components/full-screen-view-modal.react.js
+++ b/native/components/full-screen-view-modal.react.js
@@ -251,6 +251,10 @@
     const { overlayContext } = props;
     invariant(overlayContext, 'FullScreenViewModal should have OverlayContext');
     const navigationProgress = overlayContext.position;
+    invariant(
+      navigationProgress,
+      'position should be defined in FullScreenViewModal',
+    );
 
     // The inputs we receive from PanGestureHandler
     const panState = new Value(-1);
diff --git a/native/media/video-playback-modal.react.js b/native/media/video-playback-modal.react.js
--- a/native/media/video-playback-modal.react.js
+++ b/native/media/video-playback-modal.react.js
@@ -481,6 +481,10 @@
   const overlayContext = React.useContext(OverlayContext);
   invariant(overlayContext, 'VideoPlaybackModal should have OverlayContext');
   const navigationProgress = overlayContext.position;
+  invariant(
+    navigationProgress,
+    'position should be defined in VideoPlaybackModal',
+  );
 
   const reverseNavigationProgress = React.useMemo(
     () => sub(1, navigationProgress),
diff --git a/native/navigation/overlay-context.js b/native/navigation/overlay-context.js
--- a/native/navigation/overlay-context.js
+++ b/native/navigation/overlay-context.js
@@ -6,7 +6,9 @@
 export type VisibleOverlay = {
   +routeKey: string,
   +routeName: string,
-  +position: Animated.Value,
+  +position: ?Animated.Value,
+  +shouldRenderScreenContent: boolean,
+  +onExitFinish?: () => void,
   +presentedFrom: ?string,
 };
 
@@ -14,7 +16,9 @@
 
 export type OverlayContextType = {
   // position and isDismissing are local to the current route
-  +position: Animated.Node,
+  +position: ?Animated.Node,
+  +shouldRenderScreenContent: boolean,
+  +onExitFinish?: () => void,
   +isDismissing: boolean,
   // The rest are global to the entire OverlayNavigator
   +visibleOverlays: $ReadOnlyArray<VisibleOverlay>,
diff --git a/native/navigation/overlay-navigator.react.js b/native/navigation/overlay-navigator.react.js
--- a/native/navigation/overlay-navigator.react.js
+++ b/native/navigation/overlay-navigator.react.js
@@ -36,9 +36,15 @@
   OverlayRouterExtraNavigationHelpers,
   OverlayRouterNavigationAction,
 } from './overlay-router.js';
-import { scrollBlockingModals, TabNavigatorRouteName } from './route-names.js';
+import {
+  scrollBlockingModals,
+  TabNavigatorRouteName,
+  NUXTipsOverlayRouteName,
+} from './route-names.js';
 import { isMessageTooltipKey } from '../chat/utils.js';
 
+const newReanimatedRoutes = new Set([NUXTipsOverlayRouteName]);
+
 export type OverlayNavigationHelpers<ParamList: ParamListBase = ParamListBase> =
   {
     ...$Exact<StackNavigationHelpers<ParamList, {}>>,
@@ -60,7 +66,9 @@
   +route: Route<>,
   +descriptor: Descriptor<OverlayNavigationHelpers<>, {}>,
   +context: {
-    +position: Value,
+    +position: ?Value,
+    +shouldRenderScreenContent: boolean,
+    +onExitFinish?: () => void,
     +isDismissing: boolean,
   },
   +ordering: {
@@ -127,7 +135,9 @@
             descriptor,
             `OverlayNavigator could not find descriptor for ${route.key}`,
           );
-          if (!positions[route.key]) {
+          const shouldUseLegacyAnimation = !newReanimatedRoutes.has(route.name);
+
+          if (!positions[route.key] && shouldUseLegacyAnimation) {
             positions[route.key] = new Value(firstRender ? 1 : 0);
           }
           return {
@@ -136,6 +146,7 @@
             context: {
               position: positions[route.key],
               isDismissing: curIndex < routeIndex,
+              shouldRenderScreenContent: true,
             },
             ordering: {
               routeIndex,
@@ -166,6 +177,7 @@
         routeKey: route.key,
         routeName: route.name,
         position: positions[route.key],
+        shouldRenderScreenContent: true,
         presentedFrom,
       };
     };
@@ -341,50 +353,47 @@
         // A route just got dismissed
         // We'll watch the animation to determine when to clear the screen
         const { position } = data.context;
-        invariant(position, `should have position for dismissed key ${key}`);
+        const removeScreen = () => {
+          // This gets called when the scene is no longer visible and
+          // handles cleaning up our data structures to remove it
+          const curVisibleOverlays = visibleOverlaysRef.current;
+          invariant(curVisibleOverlays, 'visibleOverlaysRef should be set');
+          const newVisibleOverlays = curVisibleOverlays.filter(
+            (overlay: VisibleOverlay) => overlay.routeKey !== key,
+          );
+          if (newVisibleOverlays.length === curVisibleOverlays.length) {
+            return;
+          }
+          visibleOverlaysRef.current = newVisibleOverlays;
+          setSceneData(curSceneData => {
+            const newSceneData: { [string]: SceneData } = {};
+            for (const sceneKey in curSceneData) {
+              if (sceneKey === key) {
+                continue;
+              }
+              newSceneData[sceneKey] = {
+                ...curSceneData[sceneKey],
+                context: {
+                  ...curSceneData[sceneKey].context,
+                  visibleOverlays: newVisibleOverlays,
+                },
+              };
+            }
+            return newSceneData;
+          });
+        };
+        const listeners = position
+          ? [cond(lessOrEq(position, 0), call([], removeScreen))]
+          : [];
         updatedSceneData[key] = {
           ...data,
           context: {
             ...data.context,
             isDismissing: true,
+            shouldRenderScreenContent: false,
+            onExitFinish: removeScreen,
           },
-          listeners: [
-            cond(
-              lessOrEq(position, 0),
-              call([], () => {
-                // This gets called when the scene is no longer visible and
-                // handles cleaning up our data structures to remove it
-                const curVisibleOverlays = visibleOverlaysRef.current;
-                invariant(
-                  curVisibleOverlays,
-                  'visibleOverlaysRef should be set',
-                );
-                const newVisibleOverlays = curVisibleOverlays.filter(
-                  (overlay: VisibleOverlay) => overlay.routeKey !== key,
-                );
-                if (newVisibleOverlays.length === curVisibleOverlays.length) {
-                  return;
-                }
-                visibleOverlaysRef.current = newVisibleOverlays;
-                setSceneData(curSceneData => {
-                  const newSceneData: { [string]: SceneData } = {};
-                  for (const sceneKey in curSceneData) {
-                    if (sceneKey === key) {
-                      continue;
-                    }
-                    newSceneData[sceneKey] = {
-                      ...curSceneData[sceneKey],
-                      context: {
-                        ...curSceneData[sceneKey].context,
-                        visibleOverlays: newVisibleOverlays,
-                      },
-                    };
-                  }
-                  return newSceneData;
-                });
-              }),
-            ),
-          ],
+          listeners,
         };
         sceneDataChanged = true;
         queueAnimation(key, 0);
@@ -413,8 +422,12 @@
         return;
       }
       for (const key in pendingAnimations) {
-        const toValue = pendingAnimations[key];
         const position = positions[key];
+        if (!position) {
+          continue;
+        }
+        const toValue = pendingAnimations[key];
+
         let duration = 150;
         if (isMessageTooltipKey(key)) {
           const navigationTransitionSpec =
@@ -426,7 +439,6 @@
               navigationTransitionSpec.config.duration) ||
             400;
         }
-        invariant(position, `should have position for animating key ${key}`);
         timing(position, {
           duration,
           easing: EasingNode.inOut(EasingNode.ease),
diff --git a/native/tooltip/nux-tips-overlay.react.js b/native/tooltip/nux-tips-overlay.react.js
--- a/native/tooltip/nux-tips-overlay.react.js
+++ b/native/tooltip/nux-tips-overlay.react.js
@@ -90,7 +90,8 @@
     const dimensions = useSelector(state => state.dimensions);
     const overlayContext = React.useContext(OverlayContext);
     invariant(overlayContext, 'NUXTipsOverlay should have OverlayContext');
-    const { position } = overlayContext;
+
+    const position = React.useMemo(() => new Animated.Value(1), []);
 
     const { navigation, route } = props;
 
diff --git a/native/tooltip/tooltip.react.js b/native/tooltip/tooltip.react.js
--- a/native/tooltip/tooltip.react.js
+++ b/native/tooltip/tooltip.react.js
@@ -181,6 +181,7 @@
       const { overlayContext } = props;
       invariant(overlayContext, 'Tooltip should have OverlayContext');
       const { position } = overlayContext;
+      invariant(position, 'position should be defined in tooltip');
 
       this.backdropOpacity = interpolateNode(position, {
         inputRange: [0, 1],
@@ -432,6 +433,8 @@
 
       invariant(overlayContext, 'Tooltip should have OverlayContext');
       const { position } = overlayContext;
+      invariant(position, 'position should be defined in tooltip');
+
       const isOpeningSidebar = !!chatContext?.currentTransitionSidebarSourceID;
 
       const buttonProps: ButtonProps<BaseTooltipPropsType> = {