Gamepad API Basics for Web Games

Chrome's offline easter egg—the dinosaur game that appears when you lose connectivity—is better known than most hidden features. You can even reach it without going offline by navigating to about://dino. The game sees around 270 million plays monthly. Less known: in arcade mode you can play with a physical gamepad. Gamepad support arrived in a Chromium commit by Reilly Grant about a year before this writing, and the entire game remains open source within the Chromium project.

The Gamepad API itself has broad browser support across desktop and mobile. Feature detection is straightforward:

if ('getGamepads' in navigator) {
  // The API is supported!
}

How Browsers Model Gamepads

The browser exposes gamepads as Gamepad objects with these properties:

  • id: A string identifying the brand or style of the connected device.
  • displayId: The VRDisplay.displayId of an associated VRDisplay, if relevant.
  • index: The gamepad's index in the navigator.
  • connected: Whether the gamepad is still connected to the system.
  • hand: An enum indicating which hand the controller is held in or most likely held in.
  • timestamp: The last time the data for this gamepad was updated.
  • mapping: The button and axes mapping in use, either "standard" or "xr-standard".
  • pose: A GamepadPose object with pose information for a WebVR controller.
  • axes: An array of axis values, linearly normalized to -1.0 to 1.0.
  • buttons: An array of button states.

Individual buttons can be digital or analog, so they are represented as GamepadButton objects with their own structure:

  • pressed: Boolean pressed state.
  • touched: Boolean touched state, where the button supports touch detection.
  • value: For analog sensors, the pressed amount normalized to 0.0 through 1.0.
  • hapticActuators: An array of GamepadHapticActuator objects for haptic feedback hardware.

Some browsers and devices also expose a vibrationActuator property. It supports two kinds of rumble: dual-rumble, produced by two eccentric rotating mass actuators (one in each grip), and trigger-rumble, produced by two independent motors in the triggers.

Schematic overview of the button and axes mappings of a common gamepad.
Visual representation of a standard gamepad layout (Source).

The layout above, taken from the spec, shows the standard arrangement of buttons and axes on a generic gamepad.

Connection and Disconnection Events

To detect when a gamepad is plugged in—via USB or Bluetooth—listen for the gamepadconnected event on the window object. When it fires, a GamepadEvent carries the device details in a gamepad property:

window.addEventListener('gamepadconnected', (event) => {
  console.log('✅ 🎮 A gamepad was connected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: true
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: GamepadHapticActuator {type: "dual-rumble"}
  */
});

Disconnections work the same way, via the gamepaddisconnected event. The connected property on the event's gamepad is false in this case:

window.addEventListener('gamepaddisconnected', (event) => {
  console.log('❌ 🎮 A gamepad was disconnected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: false
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: null
  */
});

Polling in Your Game Loop

To access gamepads directly, call navigator.getGamepads(), which returns an array of Gamepad items. The array has a fixed length of four in Chrome; slots with no connected gamepad are null. Check every item, and note that a gamepad remembers its slot rather than filling the first available position:

// When no gamepads are connected:
navigator.getGamepads();
// (4) [null, null, null, null]

If navigator.getGamepads() returns null despite a connected gamepad, press any button to wake the device. Then poll states inside your game loop:

const pollGamepads = () => {
  // Always call `navigator.getGamepads()` inside of
  // the game loop, not outside.
  const gamepads = navigator.getGamepads();
  for (const gamepad of gamepads) {
    // Disregard empty slots.
    if (!gamepad) {
      continue;
    }
    // Process the gamepad state.
    console.log(gamepad);
  }
  // Call yourself upon the next animation frame.
  // (Typically this happens every 60 times per second.)
  window.requestAnimationFrame(pollGamepads);
};
// Kick off the initial game loop iteration.
pollGamepads();

Vibration Effects

The vibrationActuator property returns a GamepadHapticActuator object representing the controller's haptic motors or actuators. Call Gamepad.vibrationActuator.playEffect() to play an effect; only 'dual-rumble' and 'trigger-rumble' are valid effect types.

Dual Rumble

Dual-rumble uses unequal eccentric rotating mass motors in each handle. Because the two masses differ, their vibrations combine to form more complex patterns. The effect is defined by four parameters: duration in milliseconds, startDelay in milliseconds, plus strongMagnitude and weakMagnitude, each normalized to 0.0 through 1.0:

// This assumes a `Gamepad` as the value of the `gamepad` variable.
const dualRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  gamepad.vibrationActuator.playEffect('dual-rumble', {
    // Start delay in ms.
    startDelay: delay,
    // Duration in ms.
    duration: duration,
    // The magnitude of the weak actuator (between 0 and 1).
    weakMagnitude: weak,
    // The magnitude of the strong actuator (between 0 and 1).
    strongMagnitude: strong,
  });
};

Trigger Rumble

Trigger-rumble uses two independent motors, one in each trigger:

// This assumes a `Gamepad` as the value of the `gamepad` variable.
const triggerRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  // Feature detection.
  if (!('effects' in gamepad.vibrationActuator) || !gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
    return;
  }
  gamepad.vibrationActuator.playEffect('trigger-rumble', {
    // Duration in ms.
    duration: duration,
    // The left trigger (between 0 and 1).
    leftTrigger: leftTrigger,
    // The right trigger (between 0 and 1).
    rightTrigger: rightTrigger,
  });
};

Permissions Policy Integration

The Gamepad API spec defines a policy-controlled feature identified by the string "gamepad", with a default allowlist of "self". A document's permissions policy determines whether its content may access navigator.getGamepads(). When disabled, the gamepadconnected and gamepaddisconnected events also stop firing:

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

A Playable Demo

A gamepad tester demo lets you verify your device works in the browser; its source lives on GitHub. Connect a gamepad over USB or Bluetooth, then press buttons or move axes to see the live readings.

The same API powers a Chrome dino build with gamepad support. That demo rips the game out of the Chromium source, hosts it standalone, and extends the original implementation with ducking, vibration effects, a full-screen mode, and a dark mode contributed by Mehul Satardekar. Its polling code in trex-runner.js shows how to translate gamepad input into emulated key presses.

For further reading, the Gamepad API spec, the Gamepad API extensions spec, and the spec's GitHub repository cover the details.