How to Poll Gamepad API States in JavaScript
The Gamepad API allows web applications to interface directly with
physical game controllers in browser JavaScript. Unlike standard
event-driven DOM interfaces, active input data such as joystick
coordinates and button presses is retrieved through a continuous polling
mechanism. By calling the navigator.getGamepads() method
inside a requestAnimationFrame loop, developers can sample
and process the real-time state of connected devices in sync with the
display refresh rate.
Why the Gamepad API Uses Polling
Traditional DOM interactions rely on events like keydown
or mousemove. Controllers, however, generate high-frequency
analog and digital data that would rapidly overwhelm the JavaScript
event loop if dispatched as individual events.
To maintain performance, the Gamepad API uses events exclusively for
lifecycle changes—specifically gamepadconnected and
gamepaddisconnected. Once a controller is connected,
continuous data must be actively polled by querying the browser during
each frame of the application’s render cycle.
Setting Up the Polling Loop
The standard method for polling controller input is integrating
navigator.getGamepads() into a
requestAnimationFrame function. This method returns a
snapshot array of Gamepad objects currently recognized by
the browser.
function pollGamepad() {
const gamepads = navigator.getGamepads();
const gp = gamepads[0]; // Access the primary controller
if (gp) {
// Read button and axis states
handleButtons(gp.buttons);
handleAxes(gp.axes);
}
requestAnimationFrame(pollGamepad);
}
window.addEventListener("gamepadconnected", () => {
requestAnimationFrame(pollGamepad);
});Because navigator.getGamepads() returns a static
snapshot, the method must be called repeatedly inside the loop to
receive updated inputs.
Reading Button States
The Gamepad.buttons property returns an array of
GamepadButton objects. Each object provides detailed
information about digital and analog button interactions:
pressed: A boolean indicating whether the button is currently engaged.value: A floating-point number between0.0(fully released) and1.0(fully depressed), which allows reading pressure-sensitive inputs like analog triggers (L2/R2 or LT/RT).touched: A boolean indicating if a capacitive surface detects a finger, supported on certain modern controllers.
function handleButtons(buttons) {
buttons.forEach((button, index) => {
if (button.pressed) {
console.log(`Button ${index} is pressed with value: ${button.value}`);
}
});
}Reading Joystick and Axis States
The Gamepad.axes property contains an array of
floating-point numbers representing the physical positions of analog
sticks and directional sensors.
- Standard gamepads typically map axes in pairs: axes
0and1represent the horizontal (X) and vertical (Y) positions of the left stick, while axes2and3map the right stick. - Axis values range from
-1.0(fully left or fully up) to1.0(fully right or fully down), with0.0representing the centered resting state.
Implementing a Deadzone
Physical joysticks often suffer from slight mechanical wear, causing
“stick drift” where the resting value does not return exactly to
0.0. Implementing a software deadzone ensures
micro-movements are ignored:
function applyDeadzone(value, threshold = 0.15) {
return Math.abs(value) > threshold ? value : 0;
}
function handleAxes(axes) {
const leftStickX = applyDeadzone(axes[0]);
const leftStickY = applyDeadzone(axes[1]);
if (leftStickX !== 0 || leftStickY !== 0) {
console.log(`Left Stick Movement: X: ${leftStickX}, Y: ${leftStickY}`);
}
}Mapping Standards
Browsers use the Gamepad.mapping property to identify
input layouts. If the controller matches the standard layout defined by
the W3C specification, mapping will be set to
"standard". This ensures consistent button indices (e.g.,
index 0 for the bottom action button, index 1 for the right action
button) across different hardware vendors, such as Xbox, PlayStation,
and third-party controllers.