The Gamepad API
The Gamepad API lets browsers read input from game controllers — anything from a basic gamepad to a flight stick, racing wheel, or dance pad. It works in Chrome, Firefox, Edge, and Safari. No libraries needed.
Connecting a Gamepad
The browser doesn’t register a gamepad the moment you plug it in. It waits until a page interaction happens — usually a button press or axis movement. This is a security feature to prevent pages from detecting hardware silently.
Listen for the connection:
window.addEventListener("gamepadconnected", (event) => {
const gamepad = event.gamepad;
console.log("gamepad connected:", gamepad.id);
console.log("index:", gamepad.index);
console.log("mapping:", gamepad.mapping); // "standard" or ""
});
When the gamepad disconnects:
window.addEventListener("gamepaddisconnected", (event) => {
console.log("gamepad disconnected:", event.gamepad.index);
});
Querying Gamepad State
Unlike keyboard or mouse events, you poll the gamepad’s state in your animation loop:
function gameLoop() {
const gamepads = navigator.getGamepads();
for (const gamepad of gamepads) {
if (!gamepad) continue;
// read buttons and axes
console.log(gamepad.buttons[0].pressed); // true/false
console.log(gamepad.axes[0]); // -1 to 1
}
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
navigator.getGamepads() returns an array. The first slot is always null — it’s a quirk of the spec. Slots 1 onwards are connected gamepads, or null if that slot is empty.
Standard Gamepad Layout
The “standard” mapping (set by the browser for well-known controllers) assigns:
Buttons (0–17):
- 0: A / Cross
- 1: B / Circle
- 2: X / Square
- 3: Y / Triangle
- 4: LB / L1
- 5: RB / R1
- 6: LT / L2 (sometimes analog)
- 7: RT / R2 (sometimes analog)
- 8: Back / Select
- 9: Start
- 10: L3 (left stick click)
- 11: R3 (right stick click)
- 12–15: D-pad up/left/down/right
- 16–17: vendor-specific
Axes (0–3):
- 0: Left stick horizontal (left = -1, right = 1)
- 1: Left stick vertical (up = -1, down = 1)
- 2: Right stick horizontal
- 3: Right stick vertical
Reading Buttons
Each button in gamepad.buttons is a GamepadButton object:
const button = gamepad.buttons[0];
if (button.pressed) {
// button is currently pressed
console.log(button.value); // 0 to 1 for analog buttons (triggers)
}
Triggers on modern controllers are analog — they report a value between 0 and 1, not just pressed/released:
// Use trigger threshold to jump
if (gamepad.buttons[7].value > 0.5) {
player.jump();
}
Dead zones: cheap controllers or old hardware sometimes drift. A small input when you’re not touching the stick. Filter it out:
function applyDeadzone(value, threshold = 0.1) {
if (Math.abs(value) < threshold) return 0;
return value;
}
const x = applyDeadzone(gamepad.axes[0]);
const y = applyDeadzone(gamepad.axes[1]);
Rumble (Haptic Feedback)
If the controller supports vibration, use gamepad.vibrationActuator:
async function vibrate(gamepad, duration = 200, intensity = 0.5) {
if (gamepad.vibrationActuator) {
await gamepad.vibrationActuator.playEffect("dual-rumble", {
startDelay: 0,
duration: duration,
weakMagnitude: intensity,
strongMagnitude: intensity,
});
}
}
// Short rumble on hit
vibrate(gamepad, 100, 0.3);
// Longer rumble for low health
vibrate(gamepad, 500, 1.0);
Not all controllers support haptic feedback. Check for gamepad.vibrationActuator before calling it.
Multiple Controllers
Each gamepad gets a unique index. The first controller is 0, the second is 1, and so on. Keep track of who’s who:
const players = {};
window.addEventListener("gamepadconnected", (event) => {
const gp = event.gamepad;
players[gp.index] = {
gamepad: gp,
x: 0,
y: 0,
};
console.log(`player ${gp.index} connected`);
});
window.addEventListener("gamepaddisconnected", (event) => {
delete players[event.gamepad.index];
});
Using with Canvas or WebGL
The pattern for games: poll in your update loop, map inputs to game actions:
function update() {
const gamepads = navigator.getGamepads();
const gp = gamepads[0];
if (!gp) return;
// movement from left stick
const moveX = applyDeadzone(gp.axes[0]);
const moveY = applyDeadzone(gp.axes[1]);
player.x += moveX * speed;
player.y += moveY * speed;
// jump from A button
if (gp.buttons[0].pressed) {
player.jump();
}
// aim from right stick
aimX = applyDeadzone(gp.axes[2]);
aimY = applyDeadzone(gp.axes[3]);
requestAnimationFrame(update);
}
This replaces keyboard state tracking with gamepad state polling. The gamepad state is continuous — you get a new position every frame, not events for key down/up.
Permissions Policy
Chrome and Edge require an explicit feature policy to expose gamepad data in iframes. The parent page needs to grant it:
<iframe src="game.html" allow="gamepad"></iframe>
Without this, the iframe sees no gamepads.
Gotchas
No button press events. Unlike keyboard events, there’s no “gamepadbuttonpressed” event. You must poll navigator.getGamepads() inside requestAnimationFrame.
Index reuse. When a gamepad disconnects, its index becomes available for the next device that connects. Don’t store the index as a permanent player ID — track by the Gamepad object itself.
Pressure-sensitive buttons. Only a few buttons (triggers, sometimes face buttons) report analog values. Most buttons are 0 or 1. Check button.value for analog, button.pressed for digital.
Hidden state until interaction. gamepadconnected only fires after the user presses a button. A gamepad can be technically “connected” at the OS level but invisible to the page until first interaction.
See Also
- /guides/javascript-canvas-api/ — draw game graphics that respond to gamepad input
- /guides/javascript-web-audio-api/ — add sound effects triggered by gamepad button presses
- /guides/javascript-webgl-basics/ — 3D rendering with WebGL for full game development