Version Downloads Discord Shield
React components and hooks for creating VR/AR applications with @react-three/fiber
npm install @react-three/xr
These demos are real, you can click them! They contain the full code, too.
The following adds a button to start your session and controllers inside an XR manager to prepare your scene for WebXR rendering and interaction.
import { VRButton, ARButton, XR, Controllers, Hands } from '@react-three/xr' import { Canvas } from '@react-three/fiber' function App() { return ( <> <VRButton /> <Canvas> <XR> <Controllers /> <Hands /> <mesh> <boxGeometry /> <meshBasicMaterial color="blue" /> </mesh> </XR> </Canvas> </> ) }
<XRButton /> is an HTML <button /> that can be used to init and display info about your WebXR session. This is aliased by ARButton and VRButton with sensible session defaults.
<XRButton /* The type of `XRSession` to create */ mode={'AR' | 'VR' | 'inline'} /** * `XRSession` configuration options * @see https://immersive-web.github.io/webxr/#feature-dependencies */ sessionInit={{ optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'layers'] }} /** Whether this button should only enter an `XRSession`. Default is `false` */ enterOnly={false} /** Whether this button should only exit an `XRSession`. Default is `false` */ exitOnly={false} /** This callback gets fired if XR initialization fails. */ onError={(error) => ...} > {/* Can accept regular DOM children and has an optional callback with the XR button status (unsupported, exited, entered) */} {(status) => `WebXR ${status}`} </XRButton>
<XR /> is a WebXR manager that configures your scene for XR rendering and interaction. This lives within a R3F <Canvas />.
<Canvas> <XR /** * Enables foveated rendering. Default is `0` * 0 = no foveation, full resolution * 1 = maximum foveation, the edges render at lower resolution */ foveation={0} /** * The target framerate for the XRSystem. Smaller rates give more CPU headroom at the cost of responsiveness. * Recommended range is `72`-`120`. Default is unset and left to the device. * @note If your experience cannot effectively reach the target framerate, it will be subject to frame reprojection * which will halve the effective framerate. Choose a conservative estimate that balances responsiveness and * headroom based on your experience. * @see https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API/Rendering#refresh_rate_and_frame_rate */ frameRate={72 | 90 | 120} /** Type of WebXR reference space to use. Default is `local-floor` */ referenceSpace="local-floor" /** Called as an XRSession is requested */ onSessionStart={(event: XREvent<XRManagerEvent>) => ...} /** Called after an XRSession is terminated */ onSessionEnd={(event: XREvent<XRManagerEvent>) => ...} /** Called when an XRSession is hidden or unfocused. */ onVisibilityChange={(event: XREvent<XRSessionEvent>) => ...} /** Called when available inputsources change */ onInputSourcesChange={(event: XREvent<XRSessionEvent>) => ...} > {/* All your regular react-three-fiber elements go here */} </XR> </Canvas>
This hook gives you access to the current XRState configured by <XR />.
const { // An array of connected `XRController` controllers, // Whether the XR device is presenting in an XR session isPresenting, // Whether hand tracking inputs are active isHandTracking, // A THREE.Group representing the XR viewer or player player, // The active `XRSession` session, // `XRSession` foveation. This can be configured as `foveation` on <XR>. Default is `0` foveation, // `XRSession` reference-space type. This can be configured as `referenceSpace` on <XR>. Default is `local-floor` referenceSpace } = useXR()
To subscribe to a specific key, useXR accepts a Zustand selector:
const player = useXR((state) => state.player)
Controllers can be added with <Controllers /> for motion-controllers and/or <Hands /> for hand-tracking. These will activate whenever their respective input mode is enabled on-device and provide live models for a left and right XRController.
<Controllers /** Optional material props to pass to controllers' ray indicators */ rayMaterial={{ color: 'blue' }} /** Whether to hide controllers' rays on blur. Default is `false` */ hideRaysOnBlur={false} /** * Optional environment map to apply to controller models. * Useful for make controllers look more realistic * if you don't want to apply an env map globally on a scene */ envMap={Texture} /** * Optional environment map intensity to apply to controller models. * Useful for tweaking the env map intensity if they look too bright or dark */ envMapIntensity={1} /> <Hands // Optional custom models per hand. Default is the Oculus hand model modelLeft="/model-left.glb" modelRight="/model-right.glb" />
You can set an environment map and/or its intensity on controller models via the envMap and envMapIntensity props of <Controllers />. See ControllerEnvMap for reference.
useController references an XRController by handedness, exposing position and orientation info.
const leftController = useController('left') const rightController = useController('right') const gazeController = useController('none')
XRController is a long-living Object3D that represents an XRInputSource with the following properties:
index: number controller: THREE.XRTargetRaySpace grip: THREE.XRGripSpace hand: THREE.XRHandSpace inputSource: XRInputSource | null xrControllerModel: XRControllerModel | null
To interact with objects using controllers you can use <Interactive /> component or useInteraction hook. They allow adding controller event handlers to your objects.
<Interactive /> wraps your objects and accepts XR controller event handlers as props. Supports select, hover, blur and squeeze events (see XR inputsources).
<Interactive /* Called when hovered by a controller */ onHover={(event: XRInteractionEvent) => ...} /* Called when unhovered by a controller */ onBlur={(event: XRInteractionEvent) => ...} /* Called on button press when selected by a controller */ onSelectStart={(event: XRInteractionEvent) => ...} /* Called on button release when selected by a controller */ onSelectEnd={(event: XRInteractionEvent) => ...} /* Called on button release when another interactive is selected by a controller */ onSelectMissed={(event: XRInteractionEvent) => ...} /* Called when selected by a controller */ onSelect={(event: XRInteractionEvent) => ...} /* Called on button press when squeezed by a controller */ onSqueezeStart={(event: XRInteractionEvent) => ...} /* Called on button release when squeezed by a controller */ onSqueezeEnd={(event: XRInteractionEvent) => ...} /* Called on button release when another interactive is squeezed by a controller */ onSqueezeMissed={(event: XRInteractionEvent) => ...} /* Called when squeezed by a controller */ onSqueeze={(event: XRInteractionEvent) => ...} /* Called when a controller moves over the object, equivalent to pointermove */ onMove={(event: XRInteractionEvent) => ...} > <Box /> </Interactive>
<RayGrab /> is a specialized <Interactive /> that can be grabbed and moved by controllers.
<RayGrab> <Box /> </RayGrab>
useInteraction subscribes an existing element to controller events.
The following interaction events are supported: onHover, onBlur, onSelect, onSelectEnd, onSelectStart, onSelectMissed, onSqueeze, onSqueezeEnd, onSqueezeStart, onSqueezeMissed, onMove.
const boxRef = useRef() useInteraction(boxRef, 'onSelect', (event: XRInteractionEvent) => ...) <Box ref={boxRef} />
Use this hook to perform a hit test for an AR environment. Also see XRHitTestResult.
useHitTest((hitMatrix: Matrix4, hit: XRHitTestResult) => { // use hitMatrix to position any object on the real life surface hitMatrix.decompose(mesh.position, mesh.quaternion, mesh.scale) })
To handle controller events that are not bound to any object in the scene you can use useXREvent hook. This is a low-level abstraction that subscribes directly into the native XRInputSource (see XRInputSourceEvent).
useXREvent('squeeze', (event: XRControllerEvent) => ...)
It supports an optional third parameter with options for filtering by handedness.
useXREvent('squeeze', (event: XRControllerEvent) => ..., { handedness: 'left' | 'right' | 'none' })
While you can customize XRButton, there's a way to shave off react-dom and customize it even more. For this there's a couple of low-level utilities of a headless xr button: startSession, stopSession and toggleSession.
import { toggleSession } from '@react-three/xr' const handleClick = async () => { const session = await toggleSession('immersive-vr') if (session) { button.innerText = 'Exit VR' } else { button.innerText = 'Enter VR' } } const button = document.createElement('button') button.innerText = 'Enter VR' button.addEventListener('click', handleClick) document.appendChild(button)
To facilitate instant or accessible movement, react-xr provides teleportation helpers.
A teleportation plane with a marker that will teleport on interaction.
import { TeleportationPlane } from '@react-three/xr' ;<TeleportationPlane /** Whether to allow teleportation from left controller. Default is `false` */ leftHand={false} /** Whether to allow teleportation from right controller. Default is `false` */ rightHand={false} /** The maximum distance from the camera to the teleportation point. Default is `10` */ maxDistance={10} /** The radial size of the teleportation marker. Default is `0.25` */ size={0.25} />
Returns a TeleportCallback to teleport the player to a position.
import { useTeleportation } from '@react-three/xr' const teleport = useTeleportation() teleport([x, y, z]) teleport(new THREE.Vector3(x, y, z))