The Gamepad API

· 4 min read · Updated April 22, 2026 · intermediate
javascript gamepad gamepad-api browser haptic game-development

The Gamepad API lets browsers read input from game controllers — anything from a basic gamepad to a flight stick, racing wheel, or dance pad. It works in Chrome, Firefox, Edge, and Safari. No libraries needed.

Connecting a Gamepad

The browser doesn’t register a gamepad the moment you plug it in. It waits until a page interaction happens — usually a button press or axis movement. This is a security feature to prevent pages from detecting hardware silently.

Listen for the connection:

window.addEventListener("gamepadconnected", (event) => {
  const gamepad = event.gamepad;
  console.log("gamepad connected:", gamepad.id);
  console.log("index:", gamepad.index);
  console.log("mapping:", gamepad.mapping);  // "standard" or ""
});

When the gamepad disconnects:

window.addEventListener("gamepaddisconnected", (event) => {
  console.log("gamepad disconnected:", event.gamepad.index);
});

Querying Gamepad State

Unlike keyboard or mouse events, you poll the gamepad’s state in your animation loop:

function gameLoop() {
  const gamepads = navigator.getGamepads();

  for (const gamepad of gamepads) {
    if (!gamepad) continue;

    // read buttons and axes
    console.log(gamepad.buttons[0].pressed);  // true/false
    console.log(gamepad.axes[0]);               // -1 to 1
  }

  requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

navigator.getGamepads() returns an array. The first slot is always null — it’s a quirk of the spec. Slots 1 onwards are connected gamepads, or null if that slot is empty.

Standard Gamepad Layout

The “standard” mapping (set by the browser for well-known controllers) assigns:

Buttons (0–17):

  • 0: A / Cross
  • 1: B / Circle
  • 2: X / Square
  • 3: Y / Triangle
  • 4: LB / L1
  • 5: RB / R1
  • 6: LT / L2 (sometimes analog)
  • 7: RT / R2 (sometimes analog)
  • 8: Back / Select
  • 9: Start
  • 10: L3 (left stick click)
  • 11: R3 (right stick click)
  • 12–15: D-pad up/left/down/right
  • 16–17: vendor-specific

Axes (0–3):

  • 0: Left stick horizontal (left = -1, right = 1)
  • 1: Left stick vertical (up = -1, down = 1)
  • 2: Right stick horizontal
  • 3: Right stick vertical

Reading Buttons

Each button in gamepad.buttons is a GamepadButton object:

const button = gamepad.buttons[0];

if (button.pressed) {
  // button is currently pressed
  console.log(button.value);  // 0 to 1 for analog buttons (triggers)
}

Triggers on modern controllers are analog — they report a value between 0 and 1, not just pressed/released:

// Use trigger threshold to jump
if (gamepad.buttons[7].value > 0.5) {
  player.jump();
}

Dead zones: cheap controllers or old hardware sometimes drift. A small input when you’re not touching the stick. Filter it out:

function applyDeadzone(value, threshold = 0.1) {
  if (Math.abs(value) < threshold) return 0;
  return value;
}

const x = applyDeadzone(gamepad.axes[0]);
const y = applyDeadzone(gamepad.axes[1]);

Rumble (Haptic Feedback)

If the controller supports vibration, use gamepad.vibrationActuator:

async function vibrate(gamepad, duration = 200, intensity = 0.5) {
  if (gamepad.vibrationActuator) {
    await gamepad.vibrationActuator.playEffect("dual-rumble", {
      startDelay: 0,
      duration: duration,
      weakMagnitude: intensity,
      strongMagnitude: intensity,
    });
  }
}

// Short rumble on hit
vibrate(gamepad, 100, 0.3);

// Longer rumble for low health
vibrate(gamepad, 500, 1.0);

Not all controllers support haptic feedback. Check for gamepad.vibrationActuator before calling it.

Multiple Controllers

Each gamepad gets a unique index. The first controller is 0, the second is 1, and so on. Keep track of who’s who:

const players = {};

window.addEventListener("gamepadconnected", (event) => {
  const gp = event.gamepad;
  players[gp.index] = {
    gamepad: gp,
    x: 0,
    y: 0,
  };
  console.log(`player ${gp.index} connected`);
});

window.addEventListener("gamepaddisconnected", (event) => {
  delete players[event.gamepad.index];
});

Using with Canvas or WebGL

The pattern for games: poll in your update loop, map inputs to game actions:

function update() {
  const gamepads = navigator.getGamepads();
  const gp = gamepads[0];
  if (!gp) return;

  // movement from left stick
  const moveX = applyDeadzone(gp.axes[0]);
  const moveY = applyDeadzone(gp.axes[1]);
  player.x += moveX * speed;
  player.y += moveY * speed;

  // jump from A button
  if (gp.buttons[0].pressed) {
    player.jump();
  }

  // aim from right stick
  aimX = applyDeadzone(gp.axes[2]);
  aimY = applyDeadzone(gp.axes[3]);

  requestAnimationFrame(update);
}

This replaces keyboard state tracking with gamepad state polling. The gamepad state is continuous — you get a new position every frame, not events for key down/up.

Permissions Policy

Chrome and Edge require an explicit feature policy to expose gamepad data in iframes. The parent page needs to grant it:

<iframe src="game.html" allow="gamepad"></iframe>

Without this, the iframe sees no gamepads.

Gotchas

No button press events. Unlike keyboard events, there’s no “gamepadbuttonpressed” event. You must poll navigator.getGamepads() inside requestAnimationFrame.

Index reuse. When a gamepad disconnects, its index becomes available for the next device that connects. Don’t store the index as a permanent player ID — track by the Gamepad object itself.

Pressure-sensitive buttons. Only a few buttons (triggers, sometimes face buttons) report analog values. Most buttons are 0 or 1. Check button.value for analog, button.pressed for digital.

Hidden state until interaction. gamepadconnected only fires after the user presses a button. A gamepad can be technically “connected” at the OS level but invisible to the page until first interaction.

See Also