ViroXRSceneNavigator
A component for handling AR and VR scenes from a single navigator across platforms.
Introduced in ViroReact 2.55.0
ViroXRSceneNavigator is ViroReact's cross-reality entry point. It is a single component that, at runtime, picks the right underlying navigator for the device it's running on — mounting ViroARSceneNavigator inline on iOS and non-Quest Android, or launching VRActivity and forwarding all navigator operations to ViroVRSceneNavigator on Meta Quest. From your app's perspective there is one component, one ref, and one set of props; the platform branching is handled internally.
This is the component you should reach for whenever you want a single codebase to deliver AR on smartphones and VR on Quest headsets. Pass arInitialScene and vrInitialScene for per-platform scenes, or initialScene as shorthand when the same scene component works on both.
Platform compatibility
| Platform | Supported | Notes |
|---|---|---|
| iOS | ✅ | Renders ViroARSceneNavigator inline (ARKit) |
| Android | ✅ | Renders ViroARSceneNavigator inline (ARCore) |
| HorizonOS | ✅ | Launches VRActivity via OpenXR once, on mount. In the panel it renders an "Enter immersive view" button or your renderQuestPanel (3.0.3+; earlier versions rendered null) |
| visionOS | ❌ | Not currently supported |
Required Expo / React Native versions
ViroXRSceneNavigator enforces the VR floor at runtime — on Quest hardware with React Native below the supported version, it throws an actionable error and refuses to launch VR. AR continues to work on the lower floor.
| Path | Minimum Expo SDK | Minimum React Native |
|---|---|---|
| AR (iOS / non-Quest Android) | 54 | 0.81 |
| VR (Meta Quest) | 55 | 0.83 |
If you only need AR, you can stay on Expo 54. If your app uses VR on Quest, upgrade to Expo 55 / RN 0.83. See the Meta Quest setup guide for full configuration steps, including the xRMode: ["QUEST"] plugin option, questAppId, and VRActivity generation.
Basic example
import * as React from "react";
import { StyleSheet } from "react-native";
import { ViroXRSceneNavigator } from "@reactvision/react-viro";
import { MyARScene } from "./MyARScene";
import { MyVRScene } from "./MyVRScene";
export default function App() {
return (
<ViroXRSceneNavigator
arInitialScene={{ scene: MyARScene }}
vrInitialScene={{ scene: MyVRScene }}
style={StyleSheet.absoluteFill}
/>
);
}
On iOS / Android phones this mounts MyARScene inside ViroARSceneNavigator. On Quest, VRActivity is launched with MyVRScene as its initial scene, and the panel-side component renders the Quest panel.
Single scene across AR and VR
If you want one scene component to run on both modalities, pass initialScene instead of the platform-specific pair:
<ViroXRSceneNavigator
initialScene={{ scene: MySharedScene }}
style={StyleSheet.absoluteFill}
/>
The catch is that the two native renderers expect different scene roots: the AR path needs ViroARScene as the root (it's the bridge to ARKit/ARCore, plane detection, hit testing, camera feed) and the VR path on Quest needs ViroScene as the root. A plain ViroScene root will render nothing on AR; a ViroARScene root will render nothing on Quest.
The recommended pattern for a single shared scene component is therefore to branch at the root inside the component itself using the isQuest utility:
import { isQuest, ViroScene, ViroARScene, ViroAmbientLight, ViroBox } from "@reactvision/react-viro";
export function MySharedScene() {
const children = (
<>
<ViroAmbientLight color="#ffffff" intensity={1000} />
<ViroBox position={[0, 0, -2]} scale={[0.3, 0.3, 0.3]} />
</>
);
return isQuest ? <ViroScene>{children}</ViroScene> : <ViroARScene>{children}</ViroARScene>;
}
This is the same pattern StudioARScene uses internally to render Studio-authored content unmodified on both AR and VR.
When the user exits VR (B button, programmatic exitVRScene(), or system kill), the panel comes back. Before 3.0.3 the panel-side ViroXRSceneNavigator rendered null there, leaving a blank screen. From 3.0.3 it shows the Quest panel (below). Wire onExitViro to navigate away when VR ends:
<ViroXRSceneNavigator
vrInitialScene={{ scene: MyVRScene }}
onExitViro={() => navigation.goBack()}
/>
Quest panel (v3.0.3+)
On Quest, VRActivity launches once, on mount. Horizon OS brings the app's 2D panel forward whenever the headset view closes: Back, quitting from the system menu, going Home or opening another app's panel. None of these can be told apart from the wearer reopening the app, so the navigator does not relaunch VR on its own. (Before 3.0.3 it relaunched whenever the app became active, which reopened the headset view over Settings.) The panel shows an "Enter immersive view" button. Pass renderQuestPanel to render your own, and call enter to reopen the headset view:
<ViroXRSceneNavigator
vrInitialScene={{ scene: MyVRScene }}
onExitViro={() => navigation.goBack()}
renderQuestPanel={(enter) => <Button title="Enter VR" onPress={enter} />}
/>
Leaving VR with exitVRScene() returns to the panel through Home, where the wearer left it.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
initialScene | { scene: () => JSX.Element } | — | Scene used on both AR and VR when no platform-specific scene is provided. The scene component should branch its root between ViroARScene (AR) and ViroScene (Quest VR) using isQuest — see Single scene across AR and VR. Most apps pass arInitialScene and vrInitialScene instead. |
arInitialScene | { scene: () => JSX.Element } | — | Scene mounted on iOS / non-Quest Android via ViroARSceneNavigator. Falls back to initialScene if not provided. |
vrInitialScene | { scene: () => JSX.Element } | — | Scene mounted on Meta Quest via ViroVRSceneNavigator inside VRActivity. Forwarded through the intent bridge rather than rendered inline. Falls back to initialScene if not provided. |
worldAlignment | "Gravity" | "GravityAndHeading" | "Camera" | "Gravity" | Forwarded to ViroARSceneNavigator (AR path). |
autofocus | boolean | true | Forwarded to ViroARSceneNavigator (AR path). |
videoQuality | "High" | "Low" | — | Forwarded to ViroARSceneNavigator (AR path). |
numberOfTrackedImages | number | — | Forwarded to ViroARSceneNavigator (AR path). |
vrModeEnabled | boolean | — | Forwarded to ViroVRSceneNavigator on Quest via the intent bridge. |
passthroughEnabled | boolean | — | Forwarded to ViroVRSceneNavigator on Quest. Enables Meta Quest passthrough. |
handTrackingEnabled | boolean | — | Forwarded to ViroVRSceneNavigator on Quest. Enables XR_FB_hand_tracking_aim. |
onExitViro | () => void | — | Fires when exitVRScene() is called — from the B button, a programmatic exit, or an in-scene exit button. Use this to navigate away once VR ends. Otherwise the panel shows the Quest panel (3.0.3+) or renders blank (earlier versions). |
viroAppProps | any | — | Arbitrary props passed through to your scene component. |
hdrEnabled | boolean | — | Renderer flag — forwarded to both AR and VR paths. |
pbrEnabled | boolean | — | Renderer flag — forwarded to both AR and VR paths. |
bloomEnabled | boolean | — | Renderer flag — forwarded to both AR and VR paths. |
shadowsEnabled | boolean | — | Renderer flag — forwarded to both AR and VR paths. |
multisamplingEnabled | boolean | — | Renderer flag — forwarded to both AR and VR paths. |
debug | boolean | — | Forwarded to both AR and VR paths. |
renderQuestPanel | (enter: () => void) => React.ReactNode | "Enter immersive view" button | 3.0.3+ / Meta Quest. What the 2D panel shows while the headset view is closed. Call enter to reopen VR. See Quest panel. |
style | ViewStyle | — | Standard React Native ViewProps. On Quest, applied to the default panel only. |
ViewProps from React Native are also accepted (for example testID).
Methods
ViroXRSceneNavigator exposes a unified navigator handle through its forwarded ref. The handle is named arSceneNavigator for both paths — the name is historical, and it works on Quest as well, where every call is proxied through VRQuestNavigatorBridge to the ViroVRSceneNavigator running inside VRActivity.
const navRef = React.useRef<any>(null);
<ViroXRSceneNavigator ref={navRef} vrInitialScene={{ scene: MyVRScene }} />
// Push a new scene
navRef.current?.arSceneNavigator?.push({ scene: DetailScene });
// Pop back
navRef.current?.arSceneNavigator?.pop();
| Method | Description |
|---|---|
push(scene) | Push the given scene onto the scene stack, displaying it to the user. |
replace(scene) | Replace the top scene in the stack with the given scene. The remainder of the back history is preserved. |
jump(scene) | Move the given scene to the top of the stack, removing it from its current position. Displays it to the user. |
pop() | Pop the top-most scene off the stack, returning to the previous scene. |
popN(n) | Pop n scenes off the stack at once. popN(1) is equivalent to pop(). |
On the AR path, the underlying
ViroARSceneNavigatorinstance is exposed directly through the ref, so any additional methods documented onViroARSceneNavigator(for examplehostCloudAnchor,resolveCloudAnchor,recenterTracking) are available there too. These additional methods are AR-specific and are not bridged to Quest.
See also
Updated 3 days ago