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:

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.

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.