TypeScript icon, indicating that this package has built-in type declarations

5.7.1 • Public • Published


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.

Getting started

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 />
          <Controllers />
          <Hands />
            <boxGeometry />
            <meshBasicMaterial color="blue" />


<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.

  /* 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` */
  /** Whether this button should only exit an `XRSession`. Default is `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}`}


<XR /> is a WebXR manager that configures your scene for XR rendering and interaction. This lives within a R3F <Canvas />.

     * Enables foveated rendering. Default is `0`
     * 0 = no foveation, full resolution
     * 1 = maximum foveation, the edges render at lower resolution
     * 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` */
    /** 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 */}


This hook gives you access to the current XRState configured by <XR />.

const {
  // An array of connected `XRController`
  // Whether the XR device is presenting in an XR session
  // Whether hand tracking inputs are active
  // A THREE.Group representing the XR viewer or player
  // The active `XRSession`
  // `XRSession` foveation. This can be configured as `foveation` on <XR>. Default is `0`
  // `XRSession` reference-space type. This can be configured as `referenceSpace` on <XR>. Default is `local-floor`
} = 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.

  /** Optional material props to pass to controllers' ray indicators */
  rayMaterial={{ color: 'blue' }}
  /** Whether to hide controllers' rays on blur. Default is `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
   * Optional environment map intensity to apply to controller models.
   * Useful for tweaking the env map intensity if they look too bright or dark
  // Optional custom models per hand. Default is the Oculus hand model

Environment map

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).

  /* 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 />


<RayGrab /> is a specialized <Interactive /> that can be grabbed and moved by controllers.

  <Box />


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' })

Custom XRButton

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)


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'
  /** Whether to allow teleportation from left controller. Default is `false` */
  /** Whether to allow teleportation from right controller. Default is `false` */
  /** The maximum distance from the camera to the teleportation point. Default is `10` */
  /** The radial size of the teleportation marker. Default is `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))

Built with react-xr

  • Avatar Poser github link

Package Sidebar


npm i @react-three/xr

Weekly Downloads






Unpacked Size

334 kB

Total Files


Last publish


  • codyjasonbennett
  • sniok
  • drcmda