Develop

WebXR First Steps React sample overview

Updated: May 8, 2026

Overview

This sample demonstrates a complete VR target-shooting game built with React Three Fiber and React Three XR, covering controller input, 3D state management, spatial audio, haptic feedback, and proximity-based collision detection. The finished experience lets you aim a blaster, pull the trigger to spawn bullets, and score points as targets animate and respawn on hit.

Learning objectives

Complete this guide to learn how to:
  • WebXR session configuration with XR stores and component-to-controller mapping
  • Controller button event handling, haptic feedback, and spatial audio integration
  • Reactive state management with Zustand for cross-component game data like bullets and score
  • glTF model loading, sub-object extraction, and instance cloning for 3D assets
  • Frame-based animation and proximity collision detection without a physics engine

Requirements

To run this sample, you need a WebXR-compatible VR headset (or use the built-in desktop emulator) and a development environment with Node.js installed. The sample contains 5 TypeScript source files across focused components. See the sample README for detailed setup instructions and version requirements.

Get started

Clone or download the webxr-first-steps-react repository. Install dependencies with npm install, then start the development server with npm run dev. The server runs on port 8081 by default. If you have a VR headset, open the URL in the headset’s browser and press the VR button to enter the experience. If you are developing on desktop, the sample automatically activates built-in WebXR emulation so you can test without a headset.

Explore the sample

FileWhat it demonstratesKey concepts
src/index.tsx
Application entry point and scene composition
XR store configuration, Canvas setup, controller component mapping, VR button
src/gun.tsx
Controller-attached blaster component
Controller button events, haptic feedback, positional audio, spawning game objects
src/bullets.tsx
Bullet state management and rendering
Zustand store patterns, frame-based animation, proximity collision detection
src/targets.tsx
Target spawning and positioning
glTF cloning with useMemo, global object registry with Set<Object3D>
src/score.tsx
Score tracking and display
Zustand state updates, 3D text rendering with drei
src/assets/
Game resources
glTF models (blaster, spacestation, target), audio files, fonts

Runtime behavior

When you run this sample, you see a space station environment surrounding you with floating target objects positioned at random locations. Your right controller displays a blaster model that tracks your hand movement. When you pull the trigger, a bullet spawns from the blaster tip, accompanied by a laser sound and a short vibration pulse in your controller. The bullet travels forward in a straight line. If it passes within 1 meter of a target, the target shrinks and fades out, a hit sound plays, your score increases by 10 points, and the target respawns at a new random position after a brief delay. Your score appears as floating 3D text above the scene.

Key concepts

XR store and controller mapping

The sample creates an XR store at module scope that configures emulation settings and maps the Gun component to the right controller. When you enter VR, the Gun component renders inside the controller’s transform hierarchy, inheriting its position and rotation automatically.
const xrStore = createXRStore({
  emulate: { controller: { left: {...}, right: {...} } },
  controller: { right: Gun },
});
This pattern eliminates manual controller tracking code. See src/index.tsx for the complete setup.

Handling controller button events

The Gun component uses useXRControllerButtonEvent to respond when you press the trigger. The handler spawns a bullet, plays audio, and triggers haptic feedback through the native WebXR Gamepad API.
useXRControllerButtonEvent(state, "xr-standard-trigger", (state) => {
  if (state === "pressed") { /* spawn bullet, play sound, haptics */ }
});
Notice how the sample accesses gamepad.hapticActuators[0]?.pulse(0.6, 100) directly rather than relying on a wrapper library. See src/gun.tsx for the full button handler implementation.

Imperative state updates with Zustand

The bullet store exposes addBullet and removeBullet actions, but the Gun component accesses these imperatively via getState() rather than subscribing to reactive updates. This pattern keeps the spawn logic synchronous and avoids triggering unnecessary re-renders.
useBulletStore.getState().addBullet(worldPos, worldQuat);
The store maintains an array of bullet objects, and each Bullet component reads its own data from the array. See src/bullets.tsx for the store definition and src/gun.tsx for imperative dispatch.

Frame-based collision detection

Each Bullet component runs collision checks inside useFrame, iterating over a global Set of target objects exported from the targets module. When a bullet’s distance to a target falls below 1 meter, the bullet removes itself, animates the target, and increments the score.
const distance = target.position.distanceTo(bulletObject.position);
if (distance < 1) { /* remove bullet, animate target, add score */ }
This pattern trades global state for simplicity: targets register themselves in the set on mount, and bullets access the set directly without prop drilling. See src/bullets.tsx for hit detection logic and src/targets.tsx for target registration.

Loading and cloning glTF models

The sample demonstrates three glTF loading techniques. The spacestation uses the declarative <Gltf src="..."> component. The bullet prototype extracts a sub-object from the blaster model using useGLTF and scene.getObjectByName(). Targets call scene.clone() inside useMemo to create independent instances that can be animated separately.
const { scene } = useGLTF("assets/target.glb");
const target = useMemo(() => scene.clone(), []);
Cloning ensures each target maintains its own transform and material state. See src/targets.tsx for model cloning and src/gun.tsx for sub-object extraction.

Syncing GSAP with WebXR frames

GSAP animations run on their own ticker by default, which can desync from the WebXR frame loop. The sample solves this by calling gsap.ticker.tick() inside a minimal component that uses useFrame.
const GsapTicker = () => {
  useFrame(() => { gsap.ticker.tick(); });
  return null;
};
This ensures target shrink and respawn animations play smoothly at the headset’s refresh rate. See src/index.tsx for the ticker component and src/bullets.tsx for GSAP tween usage in hit detection.

Extend the sample

Replace the target model with your own glTF asset to customize the visual style. Add a second Gun component mapped to the left controller to enable dual-wielding. Modify the bullet spawn logic to fire multiple projectiles in a spread pattern, or add a ricochet behavior by detecting collisions with the space station geometry and reflecting the bullet’s direction vector.