Web APIs, todayLab · Alex Harto

Gamepad

Console controllers on the web, with rumble and haptic triggers.

In your browser: checking…Reviewed 1 Oct 2026

getGamepads() returns the state of each controller’s buttons and axes; with the “standard” mapping the layout is the same on Xbox, PlayStation or Switch controllers. vibrationActuator.playEffect() rumbles the controller and, with Xbox-style controllers in Chrome, the triggers too.

Reading has been Baseline since 2017 (widely available since 2019). Rumble is in Chrome, Edge and Safari on macOS; Firefox does not implement it. For privacy, no controller appears until the user presses a button.

What it is for

  • Web games with a controller
  • Controlling presentations, robots or installations
  • Accessibility with adaptive controllers

Demo

Connect a controller (USB or Bluetooth) and press any button: for privacy, the browser does not expose it until then.

Browser support

Baseline · widely available · 27 Sep 2019
FeatureDesktopMobile
ChromeEdgeFirefoxSafariChrome AndroidFirefox AndroidSafari iOS
getGamepads()35Chrome: 35 12Edge: 12 29Firefox: 29 10.1Safari: 10.1 35Chrome: 35 32Firefox: 32 10.3Safari: 10.3
Rumble (dual-rumble)68Chrome: 68 79Edge: 79 —Firefox: No16.41Safari: 16.4 68Chrome: 68 —Firefox: No—Safari: No
Triggers (trigger-rumble)1262Chrome: 126 126Edge: 126 —Firefox: No—Safari: No126Partial3Chrome: 126 Partial—Firefox: No—Safari: No
  1. 1macOS only.
  2. 2Windows and macOS; on Linux and ChromeOS, Bluetooth only. Xbox-style controllers.
  3. 3The API exists, but triggers do not rumble on Android.

Compatibility generated on every deploy from MDN browser-compat-data 8.1.3 (24 Sep 2026) · web-features 3.40.0 · checked by hand: 1 Oct 2026

Minimal code

<!doctype html>
<meta charset="utf-8">
<title>Gamepad · ejemplo mínimo</title>
<!-- Conecta un mando y pulsa un botón: el navegador solo lo muestra tras la primera pulsación. -->
<p id="status">Pulsa un botón del mando…</p>
<pre id="state"></pre>
<button id="rumble" disabled>Vibrar</button>
<script>
  const $ = (s) => document.querySelector(s);
  let index = null;

  addEventListener('gamepadconnected', (e) => {
    index = e.gamepad.index;
    $('#status').textContent = `Conectado: ${e.gamepad.id} (${e.gamepad.mapping || 'sin mapeo estándar'})`;
    $('#rumble').disabled = !e.gamepad.vibrationActuator;
    requestAnimationFrame(loop);
  });
  addEventListener('gamepaddisconnected', () => { index = null; $('#status').textContent = 'Desconectado'; });

  // El estado no llega por eventos: se consulta en cada fotograma
  function loop() {
    const pad = index === null ? null : navigator.getGamepads()[index];
    if (!pad) return;
    const pressed = pad.buttons.flatMap((b, i) => (b.pressed ? [i] : []));
    $('#state').textContent =
      `Botones pulsados: ${pressed.join(', ') || '—'}\n` +
      `Ejes: ${pad.axes.map((a) => a.toFixed(2)).join('  ')}`;
    requestAnimationFrame(loop);
  }

  // Vibración: 'trigger-rumble' en los gatillos de mandos Xbox (Chrome), si no 'dual-rumble'
  $('#rumble').onclick = () => {
    const actuator = navigator.getGamepads()[index]?.vibrationActuator;
    const effect = actuator?.effects?.includes('trigger-rumble') ? 'trigger-rumble' : 'dual-rumble';
    actuator?.playEffect(effect, {
      duration: 400, strongMagnitude: 1, weakMagnitude: 0.5, leftTrigger: 0.8, rightTrigger: 0.8,
    });
  };
</script>

What’s new

  1. Chrome 149: origin trial for a lower-latency gamepad input event.
  2. Chrome 126: trigger-rumble effect and the effects list.

Keep in mind

  1. State does not arrive through events: poll getGamepads() every frame (requestAnimationFrame).
  2. Firefox requires HTTPS. In cross-origin iframes, Chrome and Edge (103+) require allow="gamepad".
  3. In Chrome, an effect longer than 5 seconds (duration plus delay) resolves as invalid-parameter.

Further reading

Sources