Getting started
Install
pnpm add input-joystick react react-domReact 18 or later is a peer dependency. The package works with any bundler that understands the
exports map and ships both ESM and CommonJS.
Mount the joystick
Mount FloatingJoystick inside the element that should accept the touch. It renders nothing until
a touch begins.
import { FloatingJoystick, type JoystickVector } from "input-joystick";
import { useRef } from "react";
export function Game() {
const move = useRef({ x: 0, y: 0 });
return (
<div data-testid="game-viewport" style={{ position: "relative", touchAction: "none" }}>
<canvas />
<FloatingJoystick
onChange={(vector: JoystickVector) => {
move.current = { x: vector.x, y: vector.y };
}}
/>
</div>
);
}Read move.current from your game loop each frame. Set touch-action: none on the viewport so the
browser does not scroll or zoom while the thumb drags.
Choose the host
The joystick hit-tests every pointerdown against a host element. The host is the closest ancestor
of the component matching hostSelector (default [data-testid="game-viewport"]), or the
component's parent element when nothing matches. Touches that land outside the host rectangle, or
on an element outside the host, never start the stick.
<FloatingJoystick hostSelector="#playfield" onChange={onChange} />Claim only part of the screen
By default the stick takes a press anywhere in the host. Pass a claimArea to take only some of it
and leave the rest for look, drag or taps:
import { FloatingJoystick, claimWidthFraction } from "input-joystick";
<FloatingJoystick claimArea={claimWidthFraction("left", 0.4)} onChange={onChange} />;claimWidthFraction("left", 0.4) claims the left 40% of the host's width. For any other shape, pass
a function of the pointerdown and the host's rectangle that returns whether the stick claims it.
Share pointers with other controls
When a station, a button widget or a look-drag also uses pointers, give them one registry so a
finger is only ever one of them. The stick uses the shared pointerOwnership registry by default:
import { usePointerOwnership } from "input-joystick";
function LookSurface() {
const pointers = usePointerOwnership("look");
return (
<div
onPointerDown={(event) => {
if (pointers.claim(event.pointerId)) startLook(event); // false: the stick or a station has it
}}
onPointerUp={(event) => pointers.release(event.pointerId)}
/>
);
}Outside React, use pointerOwnership.claim(pointerId, owner), release and ownerOf directly, and
pointerOwnership.attach() so claims are released when a pointer ends or the window loses focus.
Keep controls tappable
A button, link or form control inside the host keeps its own taps. For anything else that must not
start the stick, such as a custom canvas widget, add data-joystick-ignore:
<div data-joystick-ignore>Inventory</div>Add keyboard parity
import { useKeyboardVectorMap } from "input-joystick";
useKeyboardVectorMap({ onChange: ({ x, y }) => (move.current = { x, y }) });By default WASD and the arrow keys map to unit vectors. Pass keys for your own bindings.
Use the math on its own
import { normalizeJoystick } from "input-joystick";
const vector = normalizeJoystick({ x: 40, y: 0 }, 58, 0.12);
// { x: 0.647..., y: 0, magnitude: 0.647..., angle: 0 }