The Gamepad API, Browser by Browser
How the Gamepad API works: poll-only snapshots, the standard mapping, Chrome's press-to-reveal gate, timestamp semantics and rumble per browser.
The model: snapshots, not events
The Gamepad API is deliberately low-level: navigator.getGamepads() returns a four-slot array of Gamepad objects (or nulls) — a snapshot of current state. There are exactly two events in the entire API, gamepadconnected and gamepaddisconnected. No buttondown, no axismove. Every consumer polls the snapshot inside requestAnimationFrame and diffs it against the previous one.
That design is also the measurement ceiling: a page samples at most once per frame, so the finest observable granularity is the frame interval — ~16.7ms on a 60Hz display.
The standard mapping
gamepad.mapping === "standard" opts into the W3C’s fixed layout, which is what makes labels possible at all:
| Index | Button | Index | Button |
|---|---|---|---|
| 0–3 | face buttons (bottom, right, left, top) | 8–9 | back/select, start |
| 4–5 | left, right bumper | 10–11 | left, right stick click |
| 6–7 | left, right trigger (analog value) |
12–15 | D-pad up/down/left/right |
| — | — | 16 | home/guide |
Axes 0–1 are the left stick, 2–3 the right, each nominally −1…+1 with +Y pointing down. Any pad that can’t conform reports a different mapping string (usually empty) — still fully readable, just unlabeled. Some non-standard pads expose triggers as extra axes rather than analog buttons.
The gesture gate
Chrome and Edge hide connected gamepads until a button on the device is pressed. Nothing is enumerable before that press — a privacy feature so web pages can’t fingerprint hardware a user never touched. Practically: instruct users to press a button once after connecting, and rely on the polling loop rather than events alone (the connect event only fires after the gate opens anyway). Firefox exposes pads on connect without the gate.
Browser-by-browser quirks
- Chrome / Edge: the gesture gate;
vibrationActuatorwithdual-rumble(andtrigger-rumbleon Xbox pads); Gamepad object mutated in place each poll; pads stop updating when the tab is hidden, andtimestampfreezes. - Firefox: exposes pads on connect; historically returned a new snapshot object per
getGamepads()call (never cache the object); vibration via the olderhapticActuators[].pulse()path; secure context required. - Safari (macOS): enumerates mainstream pads but landed vibration support late or not at all — plan for “no rumble channel”.
- iOS Safari: controller support exists in recent versions but is the thinnest implementation; test real hardware before promising anything.
- Secure context everywhere:
getGamepadsis gated to HTTPS/localhost in current engines.
gamepad.timestamp
The stamp marks when the browser engine last updated the snapshot. Watching it change across frames tells you the engine’s delivery rate — which is why this site’s update-rate panel counts timestamp changes rather than inventing a “polling rate”. Two honest caveats: engines may freeze the stamp while the pad is idle or the tab hidden, and none of it speaks to the USB wire. For the wire-level story, see polling rate.
Rumble
The modern surface is gamepad.vibrationActuator.playEffect(type, {duration, strongMagnitude, weakMagnitude}) — dual-rumble drives the two motors independently, trigger-rumble (where supported) fires Xbox trigger motors. reset() cancels. Firefox’s older hapticActuators[0].pulse(intensity, ms) covers a subset. The tester detects whichever exists — or says so plainly when neither does, which today means Firefox-on-some-pads and most of Safari.
Frequently asked questions
Why does Chrome show no gamepad until I press a button?
Anti-fingerprinting by design. Chrome only populates navigator.getGamepads() entries after a physical button press on the device, so a page can't enumerate connected hardware silently. Firefox exposes pads on connect. Either way, web pages must poll — the API never pushes input events.
Does the Gamepad API support vibration?
Yes, unevenly. The modern path is gamepad.vibrationActuator.playEffect('dual-rumble', …) with independent strong/weak motor magnitudes — Chrome and Edge implement it (Xbox pads can also take 'trigger-rumble'). Firefox exposes the older hapticActuators[].pulse() on supported pads. Safari exposes no rumble channel at all.
Is the Gamepad API event-driven?
Only for connect and disconnect — gamepadconnected/gamepaddisconnected are the only events. Buttons and axes never fire events; you poll getGamepads() inside a requestAnimationFrame loop and diff the snapshot yourself. That's exactly what our tester does every frame.
Why do I have to re-call getGamepads() every frame?
Because browser behavior differs on object lifetime: Chrome updates the Gamepad object in place, while Firefox historically returned a fresh snapshot object each call — code that cached the object once would freeze on Firefox. The safe pattern is unconditional re-polling, every frame.