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.0 —
ViroARPlaneSelectorhas been completely rewritten with a new scene-event-driven architecture. It no longer self-manages plane detection internally. You must forward anchor events fromViroARScenevia 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:
anchorDetectionTypesnow 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:
- Set
anchorDetectionTypeson yourViroARSceneto enable plane detection. - Forward
onAnchorFound,onAnchorUpdated, andonAnchorRemovedevents fromViroARSceneto theViroARPlaneSelectorinstance 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
| Type | Default | Description |
|---|---|---|
"Horizontal" | "Vertical" | "Both" | "Both" | Filter which plane types the selector displays and allows selection of. |
hideOverlayOnSelection
| Type | Default | Description |
|---|---|---|
| boolean | false | When true, the plane overlay visualization is hidden after the user selects a plane. |
material
| Type | Default | Description |
|---|---|---|
| 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
| Type | Default | Description |
|---|---|---|
| boolean | true | When true, uses the actual detected plane geometry for visualization. When false, uses a simple rectangular approximation. |
onPlaneSelected
| Type | Description |
|---|---|
(anchor: ViroAnchor, tapPosition?: number[]) => void | Called 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
| Type | Description |
|---|---|
(anchor: ViroAnchor) => boolean | void | Called 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
| Type | Description |
|---|---|
(anchor: ViroAnchor) => void | Called 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)
handleAnchorFound(anchor: ViroAnchor)Forward this from ViroARScene.onAnchorFound. Notifies the selector that a new plane anchor has been detected.
handleAnchorUpdated(anchor: ViroAnchor)
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)
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
onAnchorRemovedThe 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
hideOverlayOnSelection for Cleaner UXAfter 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
useActualShape for Accurate PlacementWith 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
anchorDetectionTypesis set onViroARSceneand 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
onAnchorRemovedwith the null guard
Children not appearing after selection
- Verify
onPlaneSelectedis being called (add aconsole.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
Updated 17 days ago