How to Poll Gamepad API Inputs in JavaScript
The Gamepad API allows web applications to interface directly with
connected controllers, but unlike typical browser inputs that rely
purely on events, controller state updates require a polling model. This
article explains the mechanics of polling gamepad states in browser
JavaScript, focusing on the distinction between connection lifecycle
events and real-time input capture using
navigator.getGamepads() within an animation loop.
Polling vs. Event-Driven Inputs
Standard web interactions (such as mouse clicks or keyboard presses)
trigger discrete event listeners like keydown or
mousemove. While the Gamepad API provides the
gamepadconnected and gamepaddisconnected
window events to notify the browser when a device is plugged in or
removed, it does not fire continuous events for button presses or
thumbstick movements.
Because controllers produce high-frequency, continuous analog and digital data, firing an event for every micro-movement would overwhelm the browser’s event loop. Instead, the Gamepad API relies on polling: the application actively queries the hardware state at a fixed interval.
The Polling
Mechanism: navigator.getGamepads()
The primary method for reading controller data is
navigator.getGamepads(). When invoked, this method returns
an array of Gamepad objects (or null for empty
slots) representing the current snapshot of connected controllers.
Because the returned Gamepad object represents a static
snapshot in time, you must call navigator.getGamepads()
repeatedly to receive updated input states. Storing a reference to a
Gamepad object will not automatically update its properties
as buttons are pressed.
Implementing
the Polling Loop with requestAnimationFrame
To ensure inputs are polled in sync with the display’s refresh rate
and without unnecessary performance overhead, developers use
requestAnimationFrame().
The standard implementation involves defining a loop function that
queries navigator.getGamepads(), processes the input
values, and schedules the next iteration:
function pollGamepad() {
const gamepads = navigator.getGamepads();
const gp = gamepads[0]; // Access the first connected controller
if (gp) {
// Process button states
gp.buttons.forEach((button, index) => {
if (button.pressed) {
console.log(`Button ${index} pressed with value: ${button.value}`);
}
});
// Process analog axes (thumbsticks)
gp.axes.forEach((axis, index) => {
// Apply a deadzone threshold to avoid drift
if (Math.abs(axis) > 0.1) {
console.log(`Axis ${index} position: ${axis}`);
}
});
}
// Continue the polling loop
requestAnimationFrame(pollGamepad);
}
// Start polling when a controller connects
window.addEventListener("gamepadconnected", (e) => {
console.log("Gamepad connected:", e.gamepad.id);
requestAnimationFrame(pollGamepad);
});Reading State: Buttons and Axes
Within each polled Gamepad snapshot, inputs are divided
into two main categories:
buttonsArray: An array ofGamepadButtonobjects. Each object provides a booleanpressedstate and a numericalvaluebetween0.0(unpressed) and1.0(fully pressed), accommodating pressure-sensitive triggers.axesArray: An array of floating-point numbers typically ranging from-1.0to1.0, representing the horizontal and vertical positions of analog thumbsticks. When polling axes, applying a software deadzone (ignoring values close to0.0) is recommended to prevent input drift caused by hardware tolerances.