HomeGuidesChangelogDiscussions
Hire a ReactVision Expert 🛠️Launch Studio 🖥️Sponsor on GitHub ❤️Log In
Guides

StudioSceneNavigator

Integrate Scenes built in ReactVision Studio into your project as either AR, VR or XR scenes using the StudioSceneNavigator

Introduced in ViroReact 2.55.0

StudioSceneNavigator is the runtime bridge between ReactVision Studio and the ViroReact renderer. It is a drop-in component that fetches a scene authored in ReactVision Studio, resolves all of its assets, animations, materials, and scene functions, and renders it cross-reality — AR on iOS and Android phones, VR on Meta Quest — using ViroXRSceneNavigator underneath.

In other words: StudioSceneNavigator is to Studio-authored scenes what ViroARSceneNavigator is to hand-coded AR scenes. Most apps built with Studio mount one StudioSceneNavigator and let it handle the rest.

Studio integration

StudioSceneNavigator requires a Studio account and a configured rvApiKey / rvProjectId in your Expo plugin config. For step-by-step setup — generating credentials, configuring the plugin, and creating a project — see the ReactVision Studio Setup guide.

The component talks to the Studio backend through VRTStudioModule.rvGetProject() and VRTStudioModule.rvGetScene(sceneId) internally — no API keys or endpoint URLs are present in your JS bundle.

Platform compatibility

PlatformSupportedNotes
iOS✅Renders the Studio scene in AR (ARKit) via ViroXRSceneNavigator's AR path
Android✅Renders the Studio scene in AR (ARCore) via ViroXRSceneNavigator's AR path
Meta Quest✅Renders the Studio scene in VR via VRActivity. StudioARScene swaps its root to ViroScene automatically on Quest
Apple visionOS❌Not currently supported

Required Expo / React Native versions

StudioSceneNavigator is built on top of ViroXRSceneNavigator and inherits the same runtime gates.

PathMinimum Expo SDKMinimum React Native
AR (iOS / non-Quest Android)540.81
VR (Meta Quest)550.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.

Very basic example

In the simplest case you can mount StudioSceneNavigator with no props at all:

import * as React from "react";
import { StudioSceneNavigator } from "@reactvision/react-viro";

export default function App() {
  return <StudioSceneNavigator />;
}

With no sceneId provided, the navigator resolves a scene to load automatically from the Studio project configured in your app's manifest (RVProjectId, set when you configure the Expo plugin — see the ReactVision Studio Setup guide). It picks a scene in the following order:

  1. The project's opening scene, if one has been set. You can configure this in Studio via Edit Project, where you can mark any scene in the project as the opening scene.
  2. If no opening scene has been set, the first scene in the project's scene list.

This is the recommended pattern for most apps: configure your project once in Studio, set an opening scene, and let StudioSceneNavigator do the rest. You can change which scene loads first by editing the project in Studio — no app code or rebuild required.

Basic example

For more control, you can load a specific scene by its Studio UUID and wire up lifecycle callbacks:

import * as React from "react";
import { StyleSheet } from "react-native";
import { StudioSceneNavigator } from "@reactvision/react-viro";

export default function App() {
  return (
    <StudioSceneNavigator
      sceneId="abc-123-uuid"
      onSceneReady={() => console.log("scene ready")}
      onError={(err) => console.error(err)}
      style={StyleSheet.absoluteFill}
    />
  );
}

Opening-scene resolution order with sceneId provided:

  1. sceneId prop.
  2. (sceneId not provided) → the project's opening scene (set in Studio via Edit Project).
  3. (No opening scene set) → the first scene in the project's scene list.

The same component runs unmodified on Quest. On Quest, StudioSceneNavigator waits for the scene data to resolve before mounting ViroXRSceneNavigator — that way VRActivity launches directly into the content scene rather than a placeholder, and a loading spinner is shown in the panel until the scene is ready. As with ViroXRSceneNavigator, always wire onExitViro so the panel can navigate away after VR ends:

<StudioSceneNavigator
  sceneId="abc-123-uuid"
  onExitViro={() => navigation.goBack()}
/>

Meta Quest panel and in-headset menu (v3.0.3+)

On Quest the scene runs in the headset view (VRActivity) while your app's 2D panel stays behind it. VR launches once, on mount. When the wearer leaves the headset view (Back/B, Home, Settings or the system menu), Horizon OS brings the panel forward and the headset view is not reopened on its own. Instead, the panel shows an "Enter immersive view" button. Pass renderQuestPanel to show your own panel, and call enter to reopen VR:

<StudioSceneNavigator
  onExitViro={() => navigation.goBack()}
  renderQuestPanel={(enter) => (
    <View style={styles.panel}>
      <Text>Scene paused</Text>
      <Button title="Back to the headset" onPress={enter} />
    </View>
  )}
  questMenuItems={[
    { label: "Restart scene", onPress: () => setSceneKey((k) => k + 1) },
    { label: "Toggle music", onPress: toggleMusic },
  ]}
/>
  • Y-button menu. Inside the headset, the left controller's Y button opens and closes a menu, centred where the wearer is looking. It shows the scene name, a shared session's status and join code, the items you pass in questMenuItems (StudioQuestMenuItem = { label: string; onPress: () => void }), and Exit. Pressing an item closes the menu. Nothing 2D can be seen from inside the headset, so put your scene actions here. The menu needs the controller: there is no hand-tracking equivalent yet. B and the left menu button still exit.
  • HUD placed in the room. The scene HUD appears in front of the wearer and stays there instead of following the head. It shows the scene name for 5 s after the scene opens, plus a shared session's status whenever it changes, including why a session failed. Its text is sharp.
  • Plane detection. Studio scenes with plane detection set to Automatic or Manual no longer crash on Quest outside a shared session (their assets render at the scene root). A plane scene opened after another AR scene now gets the room's planes.
  • Loading view. A custom loadingView fills the window on Quest, as it does on phones. The built-in spinner stays centred.
  • Leaving VR with exitVRScene() returns to the panel through Home, where the wearer left it.

Co-location (v3.0.2+)

Share a Studio-authored scene with other devices in the same physical space. Set the colocation prop — absent, the scene renders alone exactly as before.

import { StudioSceneNavigator } from "@reactvision/react-viro";
import { useRef } from "react";

function Host() {
  const ref = useRef(null);
  return (
    <StudioSceneNavigator
      ref={ref}
      colocation={{ mode: "host" }}
      onColocationStateChange={(state) => console.log(state)}
      onColocationRoom={(room) => console.log(room.joinCode)}
    />
  );
}

function Join({ code }: { code: string }) {
  return <StudioSceneNavigator colocation={{ mode: "join", code }} />;
}
  • Host scans the space and creates a room with a six-character join code. Join looks a code up and aligns to the host's space. A Quest host shares a Meta spatial anchor instead of scanning.
  • A live room shares variables, visibility, tap-to-place positions (last write wins), drags (the dragging device holds the asset), and NAVIGATE. Animation triggers and sounds play on every device while the function behind them runs on one. Collision bindings keep running when the host leaves.
  • Joiners that connect before the host has placed the scene report waiting_for_host. connectTimeoutMs (default 60000) fails a room that never comes up with CONNECT_TIMEOUT or HOST_TIMEOUT.
  • Works with an API key or a Studio session (rvSetStudioSession also reaches cloud anchors and the co-location channel). From 3.0.3, requests run in the platform database's region, falling back to unpinned if that region is down. rvSetStudioSession takes an optional functionRegion for a session on another platform URL.
  • Same-family only (phone↔phone, Quest↔Quest). On web, co-location is not available — a set colocation value reports failed with FRAME_KIND_UNSUPPORTED.
  • Built-in pill: colocationIndicator (default true). For custom UI use <StudioColocationIndicator /> or useStudioColocation(). On Quest the status and join code also show in the in-scene HUD (placed in the room from 3.0.3) and in the Y-button menu. See Meta Quest panel and in-headset menu.

See Co-Location for the lower-level frame/channel APIs used by hand-coded scenes.

Props

PropTypeDefaultDescription
sceneIdstring—UUID of a specific Studio scene to load. If omitted, the navigator fetches the project from the app manifest and uses its opening scene.
worldAlignment"Gravity" | "GravityAndHeading" | "Camera""Gravity"Forwarded to the underlying ViroXRSceneNavigator AR path.
autofocusbooleantrueForwarded to the underlying ViroXRSceneNavigator AR path.
styleViewStyleStyleSheet.absoluteFillForwarded to the underlying ViroXRSceneNavigator.
onSceneReady() => void—Fires once the Studio scene has been fetched, parsed, and mounted.
onError(err: Error) => void—Fires if the scene fetch, parse, or mount fails. If omitted, errors are logged with console.error.
onSceneChange(sceneId: string, sceneName: string) => void—Fires when navigation between Studio scenes occurs (for example via a Studio-authored NAVIGATION scene function).
onExitViro() => void—Forwarded to ViroXRSceneNavigator. Fires when exitVRScene() is called. Use this to navigate away from the screen after VR ends, otherwise it'll be blank.
renderQuestPanel(enter: () => void) => React.ReactNode"Enter immersive view" button3.0.3+ / Meta Quest. What the 2D panel shows while the headset view is closed. Call enter to reopen VR. Passed through to ViroXRSceneNavigator.
questMenuItemsreadonly StudioQuestMenuItem[] ({ label: string; onPress: () => void })—3.0.3+ / Meta Quest. Buttons listed above Exit in the in-scene menu opened with the left controller's Y button. Pressing one closes the menu.
onSceneLoaded(sceneData) => void—Fired after the scene is fetched and parsed, before it is pushed.
onPlaneDetected / onPlaneSelected() => void—Threaded to the initial scene's StudioARScene (initial scene only).
loadingViewReact.ReactNode—Opt-in overlay until the scene mounts. Omit to show the camera feed on AR; Quest falls back to a spinner.
renderError(error, retry) => React.ReactNode—Opt-in UI for a failed load or caught render error. onError still fires.
recordingIndicatorbooleantrueShow the built-in REC pill while a RECORD_VIDEO action is recording.
placementIndicatorbooleantrueShow the built-in tap-to-place prompt on mobile AR.
colocation{ mode: "host"; name?; relayUrl?; connectTimeoutMs? } | { mode: "join"; code; relayUrl?; connectTimeoutMs? }—3.0.2+. Share the scene in the same room. See Co-location.
colocationIndicatorbooleantrue3.0.2+. Show the built-in co-location pill (scan guidance / Done / join code).
onColocationStateChange(state: StudioColocationState) => void—3.0.2+. Fires as the shared session progresses (scanning, live, waiting_for_host, failed, …).
onColocationRoom(room: StudioColocationRoom) => void—3.0.2+. Once per room: when the host has its code, or when a joiner is in.
onMotionUnavailable(reason: "denied" | "no-events") => void—3.0.2+ / web. Motion access denied or no events. Never called on native.
onAssetError(asset, error) => void—3.0.2+ / web. An asset's model/image/video failed to load; the scene continues.
onRendererAbort(err) => void—3.0.2+ / web. WASM runtime aborted (often OOM); terminal until remount.
arOptionsRecord<string, unknown>—3.0.2+ / web. Forwarded to the AR session (feedWidth, feedHeight, poseSmoothing, …).
noAssetsMessagestring—Message when a scene has no placeable assets.

Methods

Via ref (StudioSceneNavigatorHandle), 3.0.2+:

MethodDescription
takeScreenshot(fileName, saveToCameraRoll)Screenshots the renderer. On Quest captures the left eye (not passthrough); may resolve errorCode RECORD_ERROR_NOT_READY until the VR scene is rendering.
leaveColocation()Leave the shared session; the current colocation prop value stays set (use retryColocation to restart).
retryColocation()Restart the current colocation value after failure or leave. Returns false while a session is running or no value is set. Re-rendering with an equal value never restarts it.
getColocationRoom()Current StudioColocationRoom or null.
finishColocationScan()Host: end the scan and host it (same as the indicator's Done). Returns true only when canFinish is true in the scanning state.

Scene-to-scene navigation in Studio-authored content is still driven by Studio NAVIGATION actions. For imperative push/pop over a hand-coded stack, use ViroXRSceneNavigator.

See also