Gamepads
gamepads API Reference
Player controllers
Each player (gamepads.p1–gamepads.p4) provides:
index-0|1|2|3gamepad- Raw Gamepad object- To detect layout/model/vendor/product from the browser
Gamepad.idstring, callidentifyGamepad(exported from@spud.gg/api) so apps that don't need identification can tree-shake the large internal ID list. For a connected player:identifyGamepad(player.gamepad?.id ?? ''). It returns:layout–GamepadBaseLayout(Nintendo|PlayStation|Xbox|Generic)generalModel–GamepadModel(generic model enum such asXbox360, including for many off-brand pads)vendorId/productId– Hex strings parsed from the gamepadidwhen knownproductName– Human-readable product string when known, otherwise'Unknown'
justConnected-trueif the gamepad just connected this framejustDisconnected-trueif the gamepad just disconnected this frameisDown(button)- Checks if a button is currently pressedwasDown(button)- Checks if a button was pressed in the previous framejustPressed(button)- Checks if a button was just pressed (current frame && not previous)justReleased(button)- Checks if a button was just released (previous frame && not current)buttonHeld(button)- Checks if a button is being held (pressed in both current and previous frame)buttonsDown- ASetof all currently pressed buttonspreviousButtonsDown- ASetof all buttons pressed in the previous frameleftStick/rightStick- Analog stick accessors with properties:x/y- Normalized values that account for deadzonesrawX/rawY- Raw axis values directly from the gamepadmagnitude- Distance from center of the analog stick (0 to 1)angle- Angle in radians (-π to π)angleDegrees- Angle in degrees (0 to 360), clockwise from rightcardinal- Nearest cardinal direction (N/NE/E/SE/S/SW/W/NW)snap4- Normalized values that snap to 4-way directions (up/down/left/right)snap8- Normalized values that snap to 8-way directions (including diagonals)deadzone- The amount of deadzone for the stickmotion(options?)- Detect directional motion with hysteresis to prevent jittery state changesoptions.activate- Threshold for activating motion (0-1, default: 0.75)options.deactivate- Threshold for deactivating motion (0-1, default: 0.25)options.diagonals- If true, returns 8-way directions; if false, returns 4-way (default: false)- Returns (4-way):
{ justMoved: { Up, Down, Left, Right }, direction: { x, y } } - Returns (8-way):
{ justMoved: { N, NE, E, SE, S, SW, W, NW }, direction: CardinalDirection | 'Neutral' }
leftTrigger/rightTrigger- Trigger accessors with properties:value- Normalized value that accounts for deadzonerawValue- Raw analog button value directly from the gamepaddeadzone- The amount of deadzone for the triggerrumble(duration?, intensity?)- Method to provide haptic feedback to the triggerduration: number - Duration in milliseconds (optional, default:30)intensity: HapticIntensity | number - Intensity of the rumble (optional, default:Balanced)
rumble(duration?, intensity?)- Easy vibration effectduration: number - Duration in milliseconds (optional, default:30)intensity: HapticIntensity - Intensity of the rumble (optional, default:Balanced)
haptic({ duration?, strong?, weak? })- Fine-grained haptic controlduration: number - Duration of haptic effect in milliseconds (optional, default:0)strong: number - Intensity of strong actuator (0-1) (optional, default:0)weak: number - Intensity of weak actuator (0-1) (optional, default:0)
hapticReset()- Cancels any ongoing haptic feedback
Examples
// check if a button is currently pressed
if (gamepads.p1.isDown(Button.South)) {
console.log('A/✕ button is being pressed');
}
// check for buttons that were just pressed this frame
if (gamepads.p2.justPressed(Button.North)) {
console.log('Y/△ button was just pressed');
}
// using analog stick values with deadzone handling
const { x, y } = gamepads.p1.leftStick;
yourGame.moveCharacter(x, y); // values are normalized with deadzone applied
// using analog stick with 8-way directional snapping
const { x: snapX, y: snapY } = gamepads.p1.rightStick.snap8;
yourGame.aimWeapon(snapX, snapY);
// using analog stick geometric properties
const { magnitude, angle, cardinal } = gamepads.p1.leftStick;
if (magnitude > 0.8) yourGame.sprint();
yourGame.rotateCamera(angle);
if (cardinal === 'NE') yourGame.openNorthEastDoor();
// using motion detection for menu navigation
const { justMoved } = gamepads.p1.leftStick.motion();
if (justMoved[Motion.Up]) yourGame.selectPreviousMenuItem();
if (justMoved[Motion.Down]) yourGame.selectNextMenuItem();
// using 8-way motion for aiming with custom activation/deactivation thresholds
const { direction, didJustMove } = gamepads.p1.rightStick.motion({
diagonals: true, // 8-way
activate: 0.5,
deactivate: 0.2,
});
if (didJustMove) {
yourGame.aimWeapon(direction);
}
// using trigger values
if (gamepads.p1.rightTrigger.value > 0.8) {
yourGame.fireWeapon();
// apply haptic feedback to the trigger
gamepads.p1.rightTrigger.rumble(100, HapticIntensity.Heavy);
}
// apply general haptic feedback
gamepads.p1.rumble(200, HapticIntensity.Balanced);
// custom haptic control
gamepads.p1.haptic({
duration: 300,
strong: 0.7,
weak: 0.3,
});
gamepads.p1.leftTrigger.rumble();
gamepads.p1.rightTrigger.rumble(50, HapticIntensity.Heavy);
// cancel all haptic effects
gamepads.p1.hapticReset();Constants and types
Button- Button constants that map to their index in the standard mapping. eg. DpadUp, Start, Select, LeftShoulder, North/South/East/West (maps to Xbox Y/A/X/B or PlayStation △/✕/□/○ or Nintendo X/B/A/Y), etc.Axis- Gamepad axes --Axis.LeftStickX,Axis.LeftStickY,Axis.RightStickX,Axis.RightStickYPlayer-Player.P1,Player.P2,Player.P3,Player.P4GamepadBaseLayout- Controller base layouts --GamepadBaseLayout.Xbox,GamepadBaseLayout.PlayStation,GamepadBaseLayout.Nintendo,GamepadBaseLayout.GenericGamepadModel- Common controller models, which several specific products can map to --GamepadModel.Xbox360,GamepadModel.PlayStationDualSense,GamepadModel.SwitchPro,GamepadModel.SteamDeck, etc.identifyGamepad(rawId: string)- Maps a browserGamepad.idstring tolayout,generalModel,vendorId,productId, andproductName. Import only when you need identification so bundlers can omit the ID database otherwise.HapticIntensity-HapticIntensity.Light,HapticIntensity.Balanced,HapticIntensity.HeavyMotion- Directional movement --Motion.Up,Motion.Down,Motion.Left,Motion.RightCardinalDirection- 8-way directions for joystick movements --CardinalDirection.N,CardinalDirection.NE,CardinalDirection.S, etc.
Examples
import {
CardinalDirection,
GamepadBaseLayout,
GamepadModel,
Button,
HapticIntensity,
Motion,
gamepads,
identifyGamepad,
} from '@spud.gg/api';
// button constants
if (gamepads.p1.justPressed(Button.South)) yourGame.jump();
if (gamepads.p1.isDown(Button.DpadLeft)) yourGame.moveLeft();
// haptic intensity levels
gamepads.p1.rumble(100, HapticIntensity.Light);
gamepads.p1.rumble(100, HapticIntensity.Balanced);
gamepads.p1.rumble(100, HapticIntensity.Heavy);
// gamepad identification (optional import — omit if you don't need layout/model detection)
const info = identifyGamepad(gamepads.p1.gamepad?.id ?? '');
if (info.layout === GamepadBaseLayout.Nintendo) {
// probably flip A/B and X/Y
}
if (info.generalModel === GamepadModel.PlayStationDualSense) {
// show touchpad in controller layout
}
// motion and direction constants (4-way)
const { direction, didJustMove } = gamepads.p1.leftStick.motion();
if (direction.x === Motion.Left) yourGame.moveLeft();
if (direction.y === Motion.Up) yourGame.moveUp();
// cardinal directions for 8-way movement
const { direction: cardinalDir } = gamepads.p1.leftStick.motion({ diagonals: true });
if (cardinalDir === CardinalDirection.NE) yourGame.moveNorthEast();Player groups
utilities for handling multiple players at once.
anyPlayer
isDown(button)- Checks if any connected player is pressing the buttonwasDown(button)- Checks if any connected player was pressing the button in the previous framejustPressed(button)- Checks if any connected player just pressed the buttonjustReleased(button)- Checks if any connected player just released the buttonbuttonHeld(button)- Checks if any connected player is holding the button
connectedPlayers
list- Array of only the connected players (gamepads that actually exist)isDown(button)- Checks if all connected players are pressing the buttonbuttonHeld(button)- Checks if all connected players are holding the buttonhaptic({ duration?, strong?, weak? })- Apply haptic feedback to all connected playersduration: number - Duration of haptic effect in milliseconds (optional, default:0)strong: number - Intensity of strong actuator (0-1) (optional, default:0)weak: number - Intensity of weak actuator (0-1) (optional, default:0)
rumble(duration?, intensity?)- Applies rumble effect to all connected playersduration: number - Duration in milliseconds (optional, default:30)intensity: HapticIntensity - Intensity of the rumble (optional, default:Balanced)
leftTrigger.rumble(duration?, intensity?)- Trigger rumble for all connected playersduration: number - Duration in milliseconds (optional, default:30)intensity: HapticIntensity - Intensity of the rumble (optional, default:Balanced)
rightTrigger.rumble(duration?, intensity?)- Trigger rumble for all connected playersduration: number - Duration in milliseconds (optional, default:30)intensity: HapticIntensity - Intensity of the rumble (optional, default:Balanced)
singlePlayer
gamepads.singleplayer is a reference to the most recently active gamepad's player. So if gamepads.p2 was the most recently active player, gamepads.singlePlayer === gamepads.p2. This allows you to easily create single-player games without needing to worry about configuring each controller. The player can seamlessly switch between any connected controllers, or connect a new one and continue playing without any additional setup. Anyone can pick up any connected controller and immediately start playing.
Examples with player groups
import { Button, gamepads, HapticIntensity, identifyGamepad, spud } from '@spud.gg/api';
// start the game when all players press start
if (gamepads.connectedPlayers.isDown(Button.Start)) {
yourGame.startGame();
}
// charge a special move when all players press a button simultaneously
if (gamepads.connectedPlayers.buttonHeld(Button.South)) {
yourGame.chargeTeamSpecialMove();
}
// apply haptic feedback to all connected players
gamepads.connectedPlayers.rumble(500, HapticIntensity.Heavy);
// or a custom haptic pattern for all players
gamepads.connectedPlayers.haptic({
duration: 200,
strong: 0.8,
weak: 0.4,
});
// 30ms trigger rumble haptic feedback for all connected players
gamepads.connectedPlayers.leftTrigger.rumble();
gamepads.connectedPlayers.rightTrigger.rumble();
// pause the game when any player presses the Select/Back button
if (gamepads.anyPlayer.justPressed(Button.Select)) {
spud.pauseGame();
}
// don't worry about p1, p2, etc. for a single-player game.
// instead just use whichever connected controller is actually triggering inputs.
const isJumping = gamepads.singlePlayer.isDown(Button.South);
// loop through just the connected controllers
gamepads.connectedPlayers.list.forEach((player) => {
const info = identifyGamepad(player.gamepad?.id ?? '');
const gp = player.gamepad;
console.log(
`${spud.players[player.index].name}: ${info.productName} ${info.generalModel} controller "${gp?.id ?? ''}"`,
);
});There's also the playerCount property showing the number of currently connected controllers. It's effectively the same as gamepads.connectedPlayers.list.length.
console.log(`${gamepads.playerCount} controller(s) connected`);players
Currently only includes the .list property, which returns a tuple containing four Player objects that have nullable gamepad properties.
players.list is the same exact thing as connectedPlayers.list but also includes disconnected players. Unlike connectedPlayers.list this will:
- always return an array of length 4
- possibly include
nullvalues for thegamepadproperty on each player in the array
clearInputs
clearInputs() saves the current state of all gamepads so wasDown, justPressed, justReleased, buttonHeld, justConnected, and justDisconnected work correctly on the next frame. Call this once per update, after your update logic and before the next update frame begins.
Angle degrees
the angle of the left analog stick in degrees in the range [0, 360)
angles are clockwise: 0° is right/east, 90° is down/south, 180° is left/west, 270° is up/north. note that positive y-axis values indicate "down" on analog sticks.
Motion
detect directional motion of the left analog stick with activation/deactivation thresholds.
this method uses "hysteresis" to prevent jittery state changes. this means movement is activated when the stick's normalized magnitude exceeds the activation threshold (0.75 by default) and is deactivated when it returns below the deactivation threshold (0.25 by default).
the api is different for 4-way vs 8-way directions.
4-way directions
- we give you both the x direction (Left | Right) and the y direction (Up | Down).
- activation thresholds are based on the normalized axis values for x and y independently.
In other words, you can have justMoved.Up and justMoved.Left at the same time (unlike in the 8-way directional version).
8-way directions
- we give you only a single cardinal direction (NE, S, SW, etc.).
- the activation threshold is the magnitude of the normalized vector that combines both x and y.
Other info
motion.justMovedvalues are only set for a single frame, so you don't need to deduplicate them.motion.directionvalues are persistent and only unset when the stick goes below the deactivation threshold.
Resume cooldown
prevent input bleed-through on resume by artificially preventing A from being pressed within the first second of being resumed unless it's been released within that timeframe.
Controller layouts
The browsers aren't much help when it comes to determining the controller model or layout, so we've been compiling our own list of gamepad layouts.
Here's the rundown: each Gamepad has an id property that will look something like "810-3-USB Gamepad" or "Xbox Elite Series 2 Wireless Controller (STANDARD GAMEPAD Vendor: 045e Product: 0b22)", depending on your browser and gamepad model. These IDs contain both a vendor ID and a USB ID as well as (sometimes) the model name. With only the vendor IDs you can get close to knowing the base layout of the controller:
const microsoftVendorId = "045e"; // xbox layout
const sonyVendorId = "054c"; // playstation layout
const nintendoVendorId = "057e"; // nintendo layoutBut there are still many other widely used gamepads with vendor IDs not included here, as well as generic vendor IDs like 810 that early PlayStation controllers used.
We've aggregated vendor and USB product IDs from...
- The Linux kernel
- Löve2D
- Unity
- Various GitHub gists and open source files
- https://github.com/elnormous/gamepads/tree/4b84620959d6d847b6264bd530e936d68e153142
- https://github.com/mdqinc/SDL_GameControllerDB/blob/master/gamecontrollerdb.txt
- https://gist.githubusercontent.com/nondebug/aec93dff7f0f1969f4cc2291b24a3171/raw/25d6cfd6f41716f909b089558c9e4941b1726c41/known_gamepads.txt
- https://raw.githubusercontent.com/360Controller/360Controller/master/360Controller/Info.plist
...and compiled and deduplicated these into a list you can access from the identifyGamepad function in the Spud API.