JavaScript Gamepad API Table

PieceWhat it doesField note
navigator.getGamepads()The poll callReturns an array with nulls; NO input events exist - you pull state every frame
gamepadconnected / disconnectedThe two eventsFire only after the user PRESSES a button - anti-fingerprinting, not a bug
gamepad.mapping === 'standard'The layout promiseStandard = Xbox-like: button 0 = A, 9 = Start, axes 0-3 = sticks; empty string = raw vendor order
buttons[i].pressed / .valueDigital + analogFace buttons boolean; TRIGGERS carry analog value 0.0-1.0 (buttons 6-7 standard)
axes[0..1] / [2..3]The stick pairsLeft stick then right stick, -1 to 1 from center; resting jitter 0.02-0.08 is normal
radial deadzone ~0.12The jitter filterRescale after the cutoff ((v - sign(v)*dz) / (1 - dz)) or the first 12% feels dead
gamepad.vibrationActuatorRumbleplayEffect('dual-rumble', {...}) - Chromium-family, no permission prompt; guard for absence
requestAnimationFrame pollThe read loopPads report 125-250Hz internally; rAF sampling reads fresh state - diff snapshots for press edges
Reference: the MDN Gamepad API guide. The polling design is deliberate: pads report at their own rate and event-per-report would flood the main thread, so you snapshot once per animation frame and diff against the previous one - that diff is where press detection lives. The press-to-connect gate is the other quirk that surprises everyone once: browsers hide gamepad data until real input, so the press-any-button prompt IS the standard UX, not a workaround.
Bottom line: trust mapping === 'standard' or nothing - one layout table (face 0-3, D-pad 12-15, analog triggers 6-7, stick clicks 10-11) covers mainstream pads; guessing vendor orders from id strings is whack-a-mole with no payoff. And skip the deadzone at your peril: resting sticks never sit at zero, so unfiltered axes become a character that drifts while the player is holding still.
Related tools: the gamepad drift tester (check YOUR sticks against the deadzone math live), the pointer events table (the mouse/touch/pen input siblings), the keyboard events table (the key side of input), the canvas table (where polled input becomes pixels), and the WebAudio table (rumble's sound-desk sibling).

The Gamepad API has no input events - that is the design fact everything else follows from. You call navigator.getGamepads() whenever you want fresh state (usually once per requestAnimationFrame frame), and you read button and axis arrays like a snapshot of a joystick board. Nothing pushes to you; you pull every frame.

Bottom line: gamepadconnected fires only after the user PRESSES A BUTTON - browsers refuse to expose devices before real input as an anti-fingerprinting measure, so your UI must say press any button instead of expecting an instant connection. And the standard mapping is the API's one sanity promise: when gamepad.mapping === 'standard', button 0 is A, 9 is Start, axes 0-3 are the sticks, on every platform that honors it.

The honest part: resting analog sticks are never perfectly zero - they jitter around 0.02-0.08 depending on hardware wear. Every deadzone you skip becomes a character that drifts when the player is not touching anything; every deadzone you overshoot feels laggy. A radial deadzone of 0.1-0.15 with smooth rescaling is the working default.

How to use

  1. Poll inside your frame loop: function tick(){ const pads = navigator.getGamepads(); for (const p of pads) if (p) readPad(p); requestAnimationFrame(tick); } - snapshots go stale between frames, so poll every frame while gameplay is active.
  2. Gate on the connect event: window.addEventListener('gamepadconnected', e => startPlayer(e.gamepad.index)) - before the first button press, getGamepads() reports the device but with empty data in some browsers; treat the event as your real start signal.
  3. Read the standard mapping: if (p.mapping === 'standard') p.buttons[0] is A/Cross, p.buttons[1] is B/Circle, p.buttons[9] is Start, p.axes[0..1] and [2..3] are the two sticks - one layout table for every mainstream pad.
  4. Apply a deadzone: const dz = (v) => Math.abs(v) < 0.12 ? 0 : (v - Math.sign(v) * 0.12) / (1 - 0.12) - the rescale keeps full throw after the deadzone instead of a dead first 12%.
  5. Rumble on hit: pad.vibrationActuator.playEffect('dual-rumble', { duration: 120, strongMagnitude: 0.8, weakMagnitude: 0.4 }) - Chromium-family support, no permission prompt; guard for absence and degrade silently.

Frequently asked questions

Why does my game never see the controller until I press a button?

Privacy by design: browsers do not expose gamepad DATA until the device produces real input, which is when the gamepadconnected event fires. Before that, getGamepads() may list the pad with no readable state - enough to know something is plugged in, not enough to read it. Ship the press-any-button prompt: it is not a workaround, it IS the standard UX; Steam's big-picture mode and every console do the same dance for the same reason.

Do I really have to poll every frame - is there no gamepadinput event?

Correct, there is none in the stable API: the committee chose polling because gamepads report at their own rate (often 125-250Hz) and event-per-report would flood the main thread. Poll once per requestAnimationFrame and diff against the previous snapshot to detect presses and releases yourself; that diff is also where you implement your own edge-detection (pressed THIS frame, not held). Chrome's data updates between rAF ticks, so frame-rate polling reads fresh state - no throttle needed at 60fps, and a 120Hz display simply samples twice as often.

What is the standard mapping and what do I do when it is missing?

mapping === 'standard' is the vendor's promise that buttons and axes follow the Xbox-style layout: face buttons 0-3, D-pad 12-15, shoulder 4-5, triggers 6-7 (analog), clicks 10-11, sticks clicks 10-11 region per spec sheet. Some pads (adapters, exotic vendors) report mapping: '' - raw hardware order. For those, either show a remap UI or refuse gracefully; guessing hardware layouts from id strings is whack-a-mole. The pragmatic call: standard-only support covers the overwhelming majority of modern pads.

How do analog triggers and sticks differ in the API?

Sticks are axes (analog pair per stick, floating -1 to 1 from center); face buttons are digital (buttons[i].pressed boolean) while TRIGGERS are buttons with analog VALUE - buttons[6].value and buttons[7].value report 0.0-1.0 trigger travel on standard-mapped pads. Use the value for driving and shooting games where half-pull matters, and remember the pressed boolean on triggers fires around the 0.5 midpoint - reading value directly gives you the finer curve.

Related tools