r3f-navigation-controls
v1.0.3
Published
Easy-to-use React component for joystick and pointer controls in 3D scenes (Three.js/React Three Fiber). Setup movement, camera rotation, and customizable UI.
Downloads
396
Maintainers
Readme
r3f-navigation-controls
A production-ready React component library for joystick, keyboard, and pointer controls in 3D scenes with React Three Fiber. Perfect for FPS games, exploration, and mobile 3D apps.
Desktop: WASD + Pointer Lock (FPS style) | Mobile: Left joystick + right-side drag to rotate
✨ Features
🎮 Dual Input Systems
- Desktop: WASD keyboard + pointer lock (FPS-style mouse look)
- Mobile: Touch joystick + drag-to-rotate camera
📱 Smart Device Detection
- Automatically detects mobile vs desktop
- Responsive to resize/orientation changes
- No joystick shown on PC, even with touch support
🎨 Fully Customizable UI
- Joystick size, colors, opacity, position
- Appearance configuration for branding
⚡ Camera-Relative Movement
- Forward/backward/left/right always follow where camera is looking
- Smooth quaternion-based rotation
🔧 Easy Integration
- Single
<NavigationController />component - One
useNavigationSetup()hook call - Drop into any React Three Fiber project
- Single
📦 No Dependencies
- Only requires React, Three.js, React Three Fiber
- Lightweight and tree-shakeable
📦 Installation
npm install r3f-navigation-controls🚀 Quick Start
Ultra-Minimal Setup (Recommended)
The Navigator component + JoystickUI handle everything automatically — mobile detection, joystick UI, pointer lock, all camera movement and rotation:
import { useRef } from 'react';
import { Canvas } from '@react-three/fiber';
import { Navigator, JoystickUI } from 'r3f-navigation-controls';
function Scene() {
return (
<>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} />
{/* Your 3D scene */}
</>
);
}
export default function App() {
const joystickDataRef = useRef({ x: 0, y: 0 });
return (
<>
<Canvas camera={{ position: [0, 5, 10] }}>
<Navigator speed={0.18} joystickDataRef={joystickDataRef} />
<Scene />
</Canvas>
<JoystickUI
joystickDataRef={joystickDataRef}
appearance={{ joystickSize: 120, primaryColor: '#00ff88' }}
/>
</>
);
}That's it! The package automatically:
- ✅ Detects if device is mobile or desktop
- ✅ Shows joystick on mobile only (rendered outside Canvas)
- ✅ Adds PointerLockControls on desktop
- ✅ Adds "Enter Pointer Lock" button on desktop
- ✅ Handles all camera movement and rotation
Advanced Setup (If You Need Custom Control)
If you need more flexibility, use the individual components:
📖 API Reference
<Navigator /> (Inside Canvas)
Handles all movement logic and rendering.
<Canvas>
<Navigator
speed={0.18}
joystickDataRef={joystickDataRef}
isMobileOverride={false} // Optional: force mobile/desktop mode
/>
<Scene />
</Canvas>What it does:
- ✅ Detects mobile vs desktop automatically
- ✅ Renders PointerLockControls on desktop (inside Canvas)
- ✅ Handles all camera movement (keyboard + joystick)
- ✅ Handles all camera rotation (pointer lock + touch drag)
- ✅ Creates "Enter Pointer Lock" button on desktop
- ✅ Returns
null(headless, no DOM inside Canvas)
Props:
speed- Movement speed (default: 0.18)joystickDataRef- Shared ref with JoystickUI (updates when joystick is dragged)isMobileOverride- Force mobile/desktop mode for testing
<JoystickUI /> (Outside Canvas)
Renders the joystick UI outside of Canvas.
<JoystickUI
joystickDataRef={joystickDataRef}
appearance={{
joystickSize: 120,
primaryColor: '#00ff88',
secondaryColor: '#00d4ff',
opacity: 0.6,
borderRadius: 50,
borderWidth: 3,
showLabels: true,
}}
/>What it does:
- ✅ Auto-detects mobile vs desktop
- ✅ Renders joystick only on mobile devices
- ✅ Completely outside Canvas (no R3F conflicts)
- ✅ Updates joystickDataRef in real-time when dragged
Props:
joystickDataRef- Shared ref with AppSetup (required)appearance- Customize colors, size, opacity, etc.joystickSize(default: 100)primaryColor(default: '#00ff88')secondaryColor(default: '#00d4ff')opacity(default: 0.6)borderRadius(default: 50)borderWidth(default: 3)showLabels(default: true)
Advanced: Individual Components
If you need more control, you can use the lower-level components:
useNavigationSetup()
const {
isMobile, // boolean - device type
usePointerLock, // boolean - pointer lock active
joystickDataRef, // ref - joystick input data
requestPointerLock, // function - request pointer lock on desktop
} = useNavigationSetup();Features:
- ✅ Mobile detection (UA + touch + coarse + viewport)
- ✅ Pointer lock state tracking
- ✅ Resize/orientation listeners
- ✅ Returns ready-to-use refs and callbacks
<Navigator /> (recommended) or <NavigationController />
Headless controller component (goes inside Canvas).
<Navigator
joystickDataRef={joystickDataRef}
speed={0.18}
isMobileOverride={false} // Optional: force mobile/desktop mode
/>What it does:
- Handles all camera movement (joystick on mobile, WASD on desktop)
- Manages touch rotation on mobile
- Returns
null(no visible DOM)
<JoystickController />
Renders the joystick UI (goes outside Canvas).
<JoystickController
isMobile={true}
joystickDataRef={joystickDataRef}
appearance={{
joystickSize: 120,
primaryColor: '#00ff88',
secondaryColor: '#00d4ff',
opacity: 0.6,
borderRadius: 50,
borderWidth: 3,
showLabels: true,
}}
/>Props:
isMobile- Show joystick when truejoystickDataRef- Shared ref with NavigationControllerappearance- Customize colors, size, position
useKeyboard()
Get keyboard state (used internally, but available).
const keyboard = useKeyboard();
// keyboard.forward, keyboard.backward, keyboard.left, keyboard.right
// keyboard.shift, keyboard.spaceusePointerControls(config?)
Get pointer/touch rotation data (used internally, but available).
const pointerRotation = usePointerControls({
sensitivity: 0.002,
minAngle: -Math.PI / 3,
maxAngle: Math.PI / 3,
});🎮 Control Scheme
Desktop
| Action | Key(s) |
|--------|--------|
| Move Forward | W / ↑ |
| Move Backward | S / ↓ |
| Strafe Left | A / ← |
| Strafe Right | D / → |
| Look Up/Down | Mouse (after pointer lock) |
| Enable Pointer Lock | Click "Enter Pointer Lock" button |
| Exit Pointer Lock | ESC |
Mobile
| Action | Input | |--------|-------| | Move | Drag left joystick | | Rotate Camera | Drag right side of screen | | Look around | Multi-direction drag |
🎨 Customization Examples
Fast Movement Speed
<NavigationController speed={0.3} joystickDataRef={joystickDataRef} />Vibrant Neon Joystick
<JoystickController
isMobile={true}
joystickDataRef={joystickDataRef}
appearance={{
joystickSize: 140,
primaryColor: '#ff00ff',
secondaryColor: '#00ffff',
opacity: 0.8,
borderRadius: 100,
borderWidth: 4,
}}
/>Force Desktop Mode (for testing)
<NavigationController
joystickDataRef={joystickDataRef}
isMobileOverride={false}
/>📱 Device Detection Logic
The package automatically detects mobile devices by checking:
- User-Agent: Mobile OS strings (Android, iOS, Windows Phone, etc.)
- Touch Support + Pointer Type: Has touch AND coarse pointer
- Viewport Size: ≤ 900px width (small screen)
Result:
- ✅ Joystick shown on: Real phones, tablets, mobile browsers
- ❌ Joystick hidden on: Desktop, touch-enabled laptops (large screens)
🔌 Full Example (Manual Control)
For advanced use cases where you want to use individual components:
import { useRef } from 'react';
import { Canvas } from '@react-three/fiber';
import { PointerLockControls } from '@react-three/drei';
import {
NavigationController,
JoystickController,
useNavigationSetup
} from 'r3f-navigation-controls';
function Scene() {
return (
<>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} />
{/* Add your 3D models and scene here */}
</>
);
}
export default function App() {
const { isMobile, usePointerLock, joystickDataRef, requestPointerLock } = useNavigationSetup();
return (
<>
<Canvas camera={{ position: [0, 5, 10] }}>
<Scene />
<Navigator joystickDataRef={joystickDataRef} />
{!isMobile && <PointerLockControls />}
</Canvas>
{!isMobile && !usePointerLock && (
<button
onClick={requestPointerLock}
style={{
position: 'absolute',
top: '20px',
right: '20px',
padding: '10px 20px',
cursor: 'pointer',
}}
>
Enable Pointer Lock
</button>
)}
{isMobile && (
<JoystickController
isMobile={true}
joystickDataRef={joystickDataRef}
appearance={{
joystickSize: 120,
primaryColor: '#00ff88',
secondaryColor: '#00d4ff',
opacity: 0.6,
}}
/>
)}
</>
);
}🛠️ Advanced: Custom Movement Logic
If you need to override movement behavior, you can access the raw hooks:
import { useKeyboard, usePointerControls } from 'r3f-navigation-controls';
import { useFrame } from '@react-three/fiber';
import { Vector3 } from 'three';
function CustomController() {
const keyboard = useKeyboard();
const pointerRotation = usePointerControls();
useFrame(({ camera }) => {
const forward = new Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
if (keyboard.forward) camera.position.add(forward.multiplyScalar(0.1));
});
return null;
}🤝 Contributing
Issues and PRs welcome! This project is maintained on GitHub.
📄 License
MIT
🙏 Credits
Built for React Three Fiber projects using Three.js and modern React hooks.
