Gamepad
Console controllers on the web, with rumble and haptic triggers.
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| Feature | Desktop | Mobile | |||||
|---|---|---|---|---|---|---|---|
| Chrome | Edge | Firefox | Safari | Chrome Android | Firefox Android | Safari 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: No | 16.41Safari: 16.4 | 68Chrome: 68 | —Firefox: No | —Safari: No |
| Triggers (trigger-rumble) | 1262Chrome: 126 | 126Edge: 126 | —Firefox: No | —Safari: No | 126Partial3Chrome: 126 Partial | —Firefox: No | —Safari: No |
- 1macOS only.
- 2Windows and macOS; on Linux and ChromeOS, Bluetooth only. Xbox-style controllers.
- 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><!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
Keep in mind
- State does not arrive through events: poll getGamepads() every frame (requestAnimationFrame).
- Firefox requires HTTPS. In cross-origin iframes, Chrome and Edge (103+) require allow="gamepad".
- In Chrome, an effect longer than 5 seconds (duration plus delay) resolves as invalid-parameter.
Further reading
Sources
- W3C · Gamepad (Editor’s Draft)accessed 1 Oct 2026
- Chrome 149 release notes2 Jun 2026
- Blink · Intent to Ship trigger-rumbleApr 2024
- MDN browser-compat-data 8.1.324 Sep 2026