Addons
import { useCommand } from '@react-three/jolt/addons';
Addons are input handling, plus a couple of helper components. They ship inside
@react-three/jolt — nothing extra to install. Its centrepiece is the
commander: you bind behaviour to a command ("jump", "move"), not to a key, and keyboard,
mouse and gamepad all feed the same command.
useCommand
import { useCommand } from '@react-three/jolt/addons';
function Player() {
useCommand(
'jump',
(info) => {
if (info.isInitial) jump();
},
() => stopJumping(),
{ keys: [' '], rate: 0.1 }
);
return null;
}
useCommand(commandString, onStart?, onEnd?, options?) registers the command (if it doesn't
already exist), subscribes both callbacks, and unsubscribes on unmount. Registration happens in
an effect, so Strict Mode's double render can't double-register, and the callbacks are read
through refs — inline arrow functions do not re-subscribe on every render.
Because registration is an effect, the hook returns undefined on the first render and the
Command from the first effect onwards. Use the returned command inside an effect, or guard it:
const command = useCommand('jump');
useEffect(() => command?.setOptions({ rate: 0.2 }), [command]);
Commands with a name in commonCommands pick up their bindings automatically:
moveForward, moveBackward, moveLeft, moveRight, jump, crouch, run, fire,
spaceFire.
CommandInfo
Every callback receives a CommandInfo — a real exported type, so info.isInitial no longer
needs a @ts-ignore:
type CommandInfo = {
command: Command;
label: string;
method: string;
value: CommandValue;
isInitial: boolean;
startTime: number;
event?: CommandEvent;
duration?: number;
};
| field | |
|---|---|
command | the Command itself |
label | its name |
method | what triggered it — the event's type, or 'update' |
value | string, number, boolean or { x, y } for vector commands (CommandValue) |
isInitial | true on the first fire of a press — what you check to avoid key repeat |
startTime | ms |
duration | ms; set on the initial down and on every up |
event | the original KeyboardEvent, MouseEvent or GamepadInputEvent (CommandEvent) |
CommandOptions
| option | |
|---|---|
keys | KeyboardEvent.key values, plus 'Mouse0'-style mouse buttons |
buttons | gamepad button indices |
axis | gamepad axis indices |
rate | minimum seconds between fires — key-repeat throttling |
isVariable, min, max, threshold, deadzone | analogue handling |
active | enable/disable without unbinding |
asVector | make it a VectorCommand — value becomes { x, y } |
preset | a named vector preset: 'move', 'look', 'race' |
bindings | overrides merged onto the preset |
inverted | { x?, y? }, flip an axis |
useCommand(
'move',
(info) => {
const { x, y } = info.value as { x: number; y: number };
move(x, y);
},
() => stop(),
{ asVector: true, preset: 'move' }
);
useCommandState
The flattened state of every live command, as a useSyncExternalStore snapshot — for HUDs,
debug overlays and anything that should re-render on input:
const state = useCommandState();
// state.jump -> boolean, state.move -> { x, y }
For per-frame reading prefer useCommand's callbacks; this one re-renders.
useLookCommand
Look and zoom from mouse, touch and gamepad — all three on by default, each registered in its own effect with a full cleanup.
import { useLookCommand } from '@react-three/jolt/addons';
useLookCommand(
(look) => rig.moveBoom(look), // THREE.Vector2 of movement deltas
(zoom) => rig.zoom(zoom),
{
mouse: true,
touch: true,
gamepad: { stick: 'right', deadzone: 0.15 },
sensitivity: { mouse: 1, touch: 1, gamepad: 200 },
invertY: false,
lockPointer: true
}
);
Both handlers are positional and required. The zoom handler is a wheel listener on the target
element and is independent of the mouse option.
| option | default | |
|---|---|---|
mouse | true | mouse drag / pointer lock |
touch | true | one-finger touch drag |
gamepad | true | false to disable, or { stick: 'left' | 'right', deadzone?: number } |
sensitivity | { mouse: 1, touch: 1, gamepad: 200 } | per source |
invertY | false | shorthand for invert.y; it wins if both are given |
invert | — | { x?, y? } |
domElement | document.body | where the listeners go |
lockPointer | false | request pointer lock on mouse down |
useAccelerated | false | use the OS pointer acceleration curve while locked |
- Mouse deltas fire while the pointer is locked or the mouse is held down.
- Touch uses pointer events filtered on
pointerType === 'touch'; a second finger (a pinch) abandons the drag rather than jumping.touch-action: noneis set on the target element while mounted and restored on cleanup. - Gamepad samples the stick once per animation frame and scales by the frame delta (so
sensitivity.gamepadis "units at full deflection per second"), clamped so a backgrounded tab can't hand it one enormous delta. The stick defaults to the right one; environments with no Gamepad API skip it silently.
The look vector handed to your handler is a single reused THREE.Vector2, and a zero delta is not
dispatched at all. Both handlers are read through refs, and every listener is removed on unmount.
useCommander and CommanderProvider
The commander attaches four window listeners and a gamepad polling loop. It is reference
counted: the first hook to mount connects it, the last to unmount disconnects it, so a screen
with no command hooks costs nothing.
By default every hook shares one lazily created commander. <CommanderProvider> scopes one to a
subtree instead — a <Canvas>, a <Physics> world, one scene of a game:
import { CommanderProvider } from '@react-three/jolt/addons';
<CommanderProvider>
<Player />
<Hud />
</CommanderProvider>;
Pass your own instance with <CommanderProvider commander={myCommander}> if you want to
configure it up front. The provider destroys the commander it created when it unmounts.
useCommander() returns whichever commander applies, for addCommand / getCommand /
addListener / removeListener / subscribe / getSnapshot.
Commander.getSnapsot (one h short) is deprecated in favour of getSnapshot. And there is no
release() method — the release function is what retain() returns.
Environments with no gamepad API (SSR, tests, some browsers) are handled — gamepad input is simply skipped.
Gamepads
Gamepad support is in-house now; the gamepad.js dependency is gone. Commander diffs
navigator.getGamepads() inside a requestAnimationFrame loop that only runs while at least one
consumer retains the commander, and stops when the last one lets go.
import { Commander, CommanderProvider } from '@react-three/jolt/addons';
const commander = new Commander({
gamepad: { deadzone: 0.2, axisThreshold: 0.02, buttonThreshold: 0.01 }
});
<CommanderProvider commander={commander}>{null}</CommanderProvider>;
CommanderOptions is { debug?: boolean; gamepad?: GamepadPollerOptions }, and
GamepadPollerOptions is:
| option | default | |
|---|---|---|
deadzone | 0.15 | axis values at or below this magnitude report as 0 |
axisThreshold | 0.01 | an axis only emits once it has moved this far from its last emitted value |
buttonThreshold | 0.01 | the same for a button's analogue value; a pressed change always emits |
debug | false |
Events keep the payload shape they always had — { type: 'gamepad:button' | 'gamepad:axis', detail: { index, button | axis, value, pressed } } — so anything written against
GamepadInputEvent is unchanged. The button detail gained an additive optional name: the W3C
standard-mapping name, which is what commonCommands binds its indices against.
import { gamepadButtonName, standardGamepadButtons, standardGamepadSticks } from '@react-three/jolt/addons';
standardGamepadButtons; // ['A', 'B', 'X', 'Y', 'LeftBumper', …, 'DPadRight', 'Home'] as const
gamepadButtonName(12); // 'DPadUp'
standardGamepadSticks; // { left: [0, 1], right: [2, 3] } — the axis indices of each stick
gamepadconnected / gamepaddisconnected are handled, several pads are tracked by index, and a
disconnect releases whatever that pad was holding, so a yanked controller can't leave a
command stuck down. hasGamepadSupport() tells you whether any of this will do anything, and
GamepadPoller is exported if you want to drive poll() from your own loop.
useGamepadForCameraControls
Drives a drei CameraControls instance from a gamepad stick:
import { CameraControls } from '@react-three/drei';
import { useGamepadForCameraControls } from '@react-three/jolt/addons';
function Rig({ controls }: { controls: CameraControls }) {
useGamepadForCameraControls('look', controls, { sensitivity: 0.03 });
return null;
}
drei is not a dependency of this package. The controls parameter is typed as
CameraControlsLike — anything with a rotate(azimuth, polar) method satisfies it, and drei's
CameraControls does so without a cast.
<Floor>
A static box floor, because every scene needs one:
import { Floor } from '@react-three/jolt/addons';
<Floor size={40} position={[0, -1, 0]} />;
Takes size, position, rotation and any <mesh> props. It is a plain static
<RigidBody> underneath — read it if you want a template for your own
helper component.