added

ViroReact 2.55.0 - HorizonOS Support And Studio Integration

Today we're shipping ViroReact 2.55.0, and it's a big one. This is the release where "build once, deploy from mobile AR to VR headsets" stops being a tagline and starts being a single component you mount.

Three things land together: first-class Meta Horizon OS support on Quest 3, Quest Pro, Quest 2 and Quest 1, a new cross-reality JS layer that auto-routes a single React surface to the right runtime, and StudioSceneNavigator — a drop-in component that pulls a ReactVision Studio scene by UUID and renders it natively across AR and VR. Plus three stability fixes, including the Android 16 KB page alignment work needed for the 2025 ABI.

Headline: a single APK now ships an Android phone/tablet AR experience and an exclusive VR Activity on Quest, from the same React codebase, with no consumer-side branching. Here's everything in the release.


Meta Horizon OS support

VR scenes run natively on Quest 3 / Quest Pro / Quest 2 / Quest 1 via a new OpenXR backend in virocore. Device-validated at 90 Hz on Quest 3, with App frame time of 5–6 ms and the full lighting pipeline — HDR, PBR, bloom, shadows — running clean.

Dual-Activity architecture. The same APK ships an Android phone/tablet panel (MainActivity) and an exclusive VR Activity (VRActivity). The Expo plugin emits the VRActivity automatically when xRMode: ["QUEST"] is set in your config. Mounting <ViroXRSceneNavigator> handles entering and exiting VR for you — no manual Activity switching.

Two simultaneous pointers. Right and left controllers (or tracked hands) each get an independent cyan laser line, hover state, and click resolution. Either side can be controller-tracked or hand-tracked at any time. Right is the single-pointer fallback when only one source is available.

Full input set. Touch controllers (triggers, grips, A/B/X/Y, menu, thumbsticks, haptics), XR_EXT_hand_tracking joints, and XR_FB_hand_tracking_aim for fingertip-aimed pointing. Pinch-to-click and grip-to-grab are detected per hand. B and Menu buttons exit VR; returning from the Quest system menu relaunches automatically — explicit exitVRScene() calls do not.

Stable hover and click on small UI targets. Pose hysteresis and a 75 ms hover grace window absorb the natural aim jitter you get from OpenXR pose streams. Hover state doesn't oscillate, and trigger pulls land on the first try even on small targets.

Passthrough and recenter.

VRModuleOpenXR.setPassthroughEnabled(viewTag, true);
VRModuleOpenXR.recenterTracking(viewTag);

Fast Refresh works inside VR. Metro Fast Refresh, timers, and requestAnimationFrame all keep working with the headset on, exactly like in the 2D panel. Iterate on VR scenes without rebuilds.

questAppId plugin option. Sideloaded builds no longer show the system "App Name Unavailable" overlay.


Cross-reality JS layer

Three additions to @reactvision/react-viro collapse the multi-platform story into a one-liner.

ViroXRSceneNavigator

Auto-detects the platform and mounts the right navigator — ViroVRSceneNavigator on Meta Quest, ViroARSceneNavigator on iOS and non-Quest Android. Pass one shared scene, or per-platform scenes:

// Shared scene across AR and VR
<ViroXRSceneNavigator initialScene={{ scene: MyScene }} />

// Or scene per platform
<ViroXRSceneNavigator
  arInitialScene={{ scene: MyARScene }}
  vrInitialScene={{ scene: MyVRScene }}
/>

Platform guards on the existing navigators

ViroARSceneNavigator short-circuits with a clean fallback when mounted on Quest instead of trying to start an AR session it can't run. ViroVRSceneNavigator does the same on non-Quest Android, instead of falling through to the deprecated Google Cardboard split-screen renderer. Both expose an override prop (questFallback / nonQuestFallback) if you want a custom UI — for example a "Launch VR" launcher button.

isQuest and hasOpenXRSupport

Runtime-detected booleans exported from the package root:

import { isQuest, hasOpenXRSupport } from "@reactvision/react-viro";

Detection is hardware-ID based (Build.MANUFACTURER, BRAND, MODEL via Platform.constants), not module-presence based — so a single APK that bundles Quest support does not misidentify a regular phone as Quest.


Multi-pointer hooks

Two hooks for apps that want aggregated pointer state without the per-source bookkeeping:

import {
  useAnySourceHover,
  useAnySourcePressed,
} from "@reactvision/react-viro";

function MyButton() {
  const [hovered, onHover] = useAnySourceHover();
  const [pressed, onClickState] = useAnySourcePressed();

  return (
    <ViroNode onHover={onHover} onClickState={onClickState} onClick={fire}>
      <ViroQuad
        scale={pressed ? [0.95, 0.95, 0.95] : [1, 1, 1]}
        materials={[hovered ? "btnHover" : "btnIdle"]}
      />
    </ViroNode>
  );
}

Both hooks return [bool, handler] and deduplicate per source ID, so a second pointer crossing an already-hovered node doesn't produce spurious enter/exit toggles in JS state. Apps that do care which specific pointer fired the event — drag-to-controller, single-handed gestures — can still read source directly from the raw callback.


StudioSceneNavigator

A drop-in component for ReactVision Studio scenes. Fetches a Studio-authored scene by UUID via rvGetScene(sceneId). Auth is the project's API key, wired through the Expo plugin (rvProjectId in app.json):

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

<StudioSceneNavigator
  sceneId="abc-123-uuid"
  style={StyleSheet.absoluteFill}
/>;

What it renders, end to end:

  • 3D models (GLB / VRX), images, video, and text from the scene's asset registry
  • Per-asset placement (position, rotation, scale) and physics bodies
  • Scene functions: NAVIGATION (push to another scene), ALERT, ANIMATION
  • Collision bindings (asset-pair triggered functions)
  • Animation registry — Studio-authored keyframes pushed into ViroAnimations
  • Material configs with optional shader modifiers, including an animated time uniform for animated presets, _rf_vpw / _rf_vph viewport uniforms for screen-space effects, and auto-flagged requiresCameraTexture when the shader samples the camera feed
  • Image-tracking targets and the Viro360Image / Viro360Video background
  • Physics world configs

StudioSceneNavigator is fully cross-reality on Quest. The same sceneId mounts the scene in AR on phones and tablets, and in VR on headset, with no consumer-side branching.

The API underneath

Two new endpoints in the ReactVision Cloud Anchor SDK (libreactvisioncca, shipped inside virocore):

  • getScene(sceneId, callback) — full scene response: metadata, assets, animations, collision bindings, scene functions, project info.
  • getSceneAssets(sceneId, callback) — asset-list-only variant for clients that already have scene metadata cached.

Both are exposed to JS via ViroARSceneNavigator.rvGetScene(sceneId) and rvGetSceneAssets(sceneId), and authenticate with the project API key wired through the Expo plugin. Existing Cloud Anchor and Geospatial endpoints are unchanged.


Fixes

16 KB .so page alignment for libvrapi.so. The Android 2025 ABI requires all shipped .so files to align to 16 KB pages. libvrapi.so is now repackaged with -Wl,-z,max-page-size=16384, resolving load failures on devices that ship with the new page size.

ViroARImageMarker children fixed-on-screen after re-detection (Android — #465). Models parented to an image marker no longer pin to screen coordinates after the target is lost and re-acquired — a regression in 2.54.0. Markers re-anchor cleanly to the detected world pose every time, including on subsequent re-detection.

iOS ViroPortalScene portal-tree stability (#452). Continued portal-render-pass hardening on top of the 2.54.0 fix:

  • Portal stencil silhouette no longer drops transparent entry fragments before alpha discard runs.
  • 360° background inside a portal is no longer overwritten by the AR camera background drawn afterwards.
  • The interior of a portal hole no longer reveals the portal interior when the user is outside a nested exit-frame portal.
  • AR occlusion is disabled inside the portal interior, so virtual content is no longer discarded by depth-based occlusion when nested.

Compatibility

  • React Native 0.83, Expo 54 + Expo 55 for AR and non-Quest paths (unchanged from 2.54.0).
  • Android Gradle Plugin 8.7+ is required for 16 KB-aligned APKs. Minimum SDK unchanged (24).
  • Meta Quest support requires React Native ≥ 0.83 / Expo ≥ 55. ViroXRSceneNavigator throws an actionable JS error if the runtime is below the threshold. AR continues to work on the Expo 54 + RN 0.79 baseline.
  • Meta Quest support also requires the Quest variant of the Viro Android packageReactViroPackage(ReactViroPackage.ViroPlatform.QUEST) in MainApplication.kt, plus xRMode: ["QUEST"] in the Expo plugin config. Both are emitted automatically by expo prebuild when the plugin is configured.
  • Recommended install path: Expo Dev Client. Bare React Native is not tested for this release. It should work but needs substantial manual wiring — MainApplication.kt package registration, the VRActivity Android Activity declaration with the correct intent filter and Quest hardware features, the xRMode Quest manifest features, and iOS Podfile configuration. The Expo plugin generates all of these automatically. Bare RN support will be revisited in a follow-up release.
  • iOS and non-Quest Android code paths are unchanged. Existing apps upgrade with no code changes.

Migration

No breaking JS API changes. Existing ViroARSceneNavigator and ViroVRSceneNavigator usage continues to work as-is.

New cross-reality apps should mount <ViroXRSceneNavigator> and wire its onExitViro prop to take the panel back to wherever VR was launched from (for example, navigation.goBack()). B and Menu buttons exit VR automatically; exitVRScene() from @reactvision/react-viro is only needed for programmatic in-scene exits.


Upgrade

npm install @reactvision/[email protected]

Full release notes and binaries on GitHub. Docs at viro-community.readme.io. Questions? We hang out in Discord.

We've been working towards "phones to headsets from one codebase" for a long time. With 2.55.0 it's real, it's shipping, and we can't wait to see what you build on it.