ViroARPlaneSelector

ViroARPlaneSelector

A component that displays detected AR planes and lets the user select one by tapping. When a plane is selected, your child components are placed at the selected location. This is commonly used for placing AR content on surfaces like floors, tables, and walls.

⚠️

Rewritten in v2.53.0ViroARPlaneSelector has been completely rewritten with a new scene-event-driven architecture. It no longer self-manages plane detection internally. You must forward anchor events from ViroARScene via refs.


Basic Usage

import React, { useRef } from "react";
import {
  ViroARSceneNavigator,
  ViroARScene,
  ViroARPlaneSelector,
  ViroBox,
} from "@reactvision/react-viro";

const PlacementScene = () => {
  const selectorRef = useRef<ViroARPlaneSelector>(null);

  return (
    <ViroARScene
      anchorDetectionTypes={["PlanesHorizontal", "PlanesVertical"]}
      onAnchorFound={(a) => selectorRef.current?.handleAnchorFound(a)}
      onAnchorUpdated={(a) => selectorRef.current?.handleAnchorUpdated(a)}
      onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}
    >
      <ViroARPlaneSelector
        ref={selectorRef}
        alignment="Both"
        onPlaneSelected={(anchor, tapPosition) => {
          console.log("Plane selected:", anchor.anchorId);
          if (tapPosition) {
            console.log("Tap position:", tapPosition);
          }
        }}
      >
        {/* Children are placed at the selected plane location */}
        <ViroBox
          position={[0, 0.05, 0]}
          scale={[0.1, 0.1, 0.1]}
          materials={["boxMaterial"]}
        />
      </ViroARPlaneSelector>
    </ViroARScene>
  );
};

const App = () => (
  <ViroARSceneNavigator initialScene={{ scene: PlacementScene }} />
);

export default App;

Note: anchorDetectionTypes now defaults to detecting both horizontal and vertical planes as of v2.53.0.


Architecture

In v2.53.0, ViroARPlaneSelector uses a scene-event-driven architecture. Rather than managing plane detection internally, you must:

  1. Set anchorDetectionTypes on your ViroARScene to enable plane detection.
  2. Forward onAnchorFound, onAnchorUpdated, and onAnchorRemoved events from ViroARScene to the ViroARPlaneSelector instance via its ref.

This gives you full control over anchor detection and lets you use the same anchor events for other purposes alongside the plane selector.

const selectorRef = useRef<ViroARPlaneSelector>(null);

<ViroARScene
  anchorDetectionTypes={["PlanesHorizontal", "PlanesVertical"]}
  onAnchorFound={(a) => selectorRef.current?.handleAnchorFound(a)}
  onAnchorUpdated={(a) => selectorRef.current?.handleAnchorUpdated(a)}
  onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}
>
  <ViroARPlaneSelector ref={selectorRef} ... />
</ViroARScene>

Props

alignment

TypeDefaultDescription
"Horizontal" | "Vertical" | "Both""Both"Filter which plane types the selector displays and allows selection of.

hideOverlayOnSelection

TypeDefaultDescription
booleanfalseWhen true, the plane overlay visualization is hidden after the user selects a plane.

material

TypeDefaultDescription
string(built-in)Name of a custom ViroMaterial to use for the plane visualization overlay. If not set, a default semi-transparent material is used.

useActualShape

TypeDefaultDescription
booleantrueWhen true, uses the actual detected plane geometry for visualization. When false, uses a simple rectangular approximation.

onPlaneSelected

TypeDescription
(anchor: ViroAnchor, tapPosition?: number[]) => voidCalled when the user taps on a detected plane. Receives the plane anchor and an optional tapPosition array [x, y, z] indicating where on the plane the user tapped. Objects are now placed at the exact tap point, not the plane centre.

onPlaneDetected

TypeDescription
(anchor: ViroAnchor) => boolean | voidCalled when a new plane is detected. Return false to prevent the plane from being added to the visible set. Return true or void to allow it.

This is useful for filtering which planes appear in the selector:

onPlaneDetected={(anchor) => {
  // Only show large planes (e.g. floors, tables)
  if (anchor.width < 0.5 || anchor.height < 0.5) {
    return false; // reject small planes
  }
  return true;
}}

onPlaneRemoved

TypeDescription
(anchor: ViroAnchor) => voidCalled when a previously detected plane is removed.

Ref Methods

These methods are called on the ViroARPlaneSelector ref and should be connected to the corresponding ViroARScene anchor callbacks.

handleAnchorFound(anchor: ViroAnchor)

Forward this from ViroARScene.onAnchorFound. Notifies the selector that a new plane anchor has been detected.

handleAnchorUpdated(anchor: ViroAnchor)

Forward this from ViroARScene.onAnchorUpdated. Notifies the selector that an existing plane anchor has been updated (e.g. its boundaries have changed).

handleAnchorRemoved(anchor: ViroAnchor)

Forward this from ViroARScene.onAnchorRemoved. Notifies the selector that a plane anchor has been removed.


Migration from v2.52.x

Breaking Changes

Removed prop: maxPlanes — This prop has been removed. The selector now displays all planes forwarded to it via anchor events. If you need to limit the number of visible planes, filter them in your onAnchorFound handler before forwarding to the selector.

Required: Forwarding anchor events — The selector no longer manages plane detection internally. You must set up the anchor event forwarding pattern shown above.

Changed: onPlaneSelected signature — The callback now includes an optional tapPosition parameter indicating the exact tap location on the plane.

Before (v2.52.x)

// Old approach — ViroARPlaneSelector managed its own detection
<ViroARScene>
  <ViroARPlaneSelector
    maxPlanes={5}
    onPlaneSelected={(anchor) => { ... }}
  >
    <ViroBox ... />
  </ViroARPlaneSelector>
</ViroARScene>

After (v2.53.0)

// New approach — forward anchor events via refs
const selectorRef = useRef<ViroARPlaneSelector>(null);

<ViroARScene
  anchorDetectionTypes={["PlanesHorizontal", "PlanesVertical"]}
  onAnchorFound={(a) => selectorRef.current?.handleAnchorFound(a)}
  onAnchorUpdated={(a) => selectorRef.current?.handleAnchorUpdated(a)}
  onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}
>
  <ViroARPlaneSelector
    ref={selectorRef}
    alignment="Both"
    onPlaneSelected={(anchor, tapPosition) => { ... }}
  >
    <ViroBox ... />
  </ViroARPlaneSelector>
</ViroARScene>

Best Practices

1. Always Forward All Three Anchor Events

Ensure you forward onAnchorFound, onAnchorUpdated, and onAnchorRemoved. Missing any of these can cause stale planes, ghost planes, or planes that never update their boundaries.

2. Guard Against Null Anchors in onAnchorRemoved

The removal callback can occasionally receive a null anchor. Always guard before forwarding:

onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}

3. Use hideOverlayOnSelection for Cleaner UX

After the user selects a plane, the overlay visualization is usually no longer needed. Set hideOverlayOnSelection={true} to automatically clean up the visual indicators.

4. Prefer useActualShape for Accurate Placement

With useActualShape={true} (the default), the plane visualization matches the actual detected shape of the surface. This gives users a more accurate sense of where content will be placed.


Troubleshooting

No planes appearing

  • Ensure anchorDetectionTypes is set on ViroARScene and includes "PlanesHorizontal" and/or "PlanesVertical"
  • Verify all three anchor event handlers are connected to the selector ref
  • Check that the device is moving slowly to allow plane detection
  • Ensure good lighting conditions

Ghost planes or stale planes

  • This was a known bug in v2.52.x and has been fixed in v2.53.0
  • Ensure you are forwarding onAnchorRemoved with the null guard

Children not appearing after selection

  • Verify onPlaneSelected is being called (add a console.log)
  • Check that child components have valid positions and materials
  • Ensure children are direct descendants of ViroARPlaneSelector

Planes flicker or jump

  • This can happen in low-light conditions or on featureless surfaces
  • The adaptive throttle improvements in v2.53.0 should reduce this
  • Try using useActualShape={false} for a more stable (but less accurate) visualization