Addons

useCommand, useLookCommand and the commander — input mapped to named commands.
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.

Important

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
commandthe Command itself
labelits name
methodwhat triggered it — the event's type, or 'update'
valuestring, number, boolean or { x, y } for vector commands (CommandValue)
isInitialtrue on the first fire of a press — what you check to avoid key repeat
startTimems
durationms; set on the initial down and on every up
eventthe original KeyboardEvent, MouseEvent or GamepadInputEvent (CommandEvent)

CommandOptions

option
keysKeyboardEvent.key values, plus 'Mouse0'-style mouse buttons
buttonsgamepad button indices
axisgamepad axis indices
rateminimum seconds between fires — key-repeat throttling
isVariable, min, max, threshold, deadzoneanalogue handling
activeenable/disable without unbinding
asVectormake it a VectorCommand — value becomes { x, y }
preseta named vector preset: 'move', 'look', 'race'
bindingsoverrides 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.

optiondefault
mousetruemouse drag / pointer lock
touchtrueone-finger touch drag
gamepadtruefalse to disable, or { stick: 'left' | 'right', deadzone?: number }
sensitivity{ mouse: 1, touch: 1, gamepad: 200 }per source
invertYfalseshorthand for invert.y; it wins if both are given
invert—{ x?, y? }
domElementdocument.bodywhere the listeners go
lockPointerfalserequest pointer lock on mouse down
useAcceleratedfalseuse 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: none is 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.gamepad is "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.

Note

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:

optiondefault
deadzone0.15axis values at or below this magnitude report as 0
axisThreshold0.01an axis only emits once it has moved this far from its last emitted value
buttonThreshold0.01the same for a button's analogue value; a pressed change always emits
debugfalse

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.