Learn how to use the Gamepad API to push your web games to the next level.
Chrome's offline page easter egg is one of the worst-kept secrets in history ([citation needed]
,
but claim made for the dramatic effect). If you press the space key or, on mobile
devices, tap the dinosaur, the offline page becomes a playable arcade game. You might be aware that
you don't actually have to go offline when you feel like playing: in Chrome, you can just navigate
to about://dino
, or, for the geek in you, browse to about://network-error/-106
. But did you know
that there are
270 million Chrome dino games played every month?
Another fact that arguably is more useful to know and that you might not be aware of is that in arcade mode you can play the game with a gamepad. Gamepad support was added roughly one year ago as of the time of this writing in a commit by Reilly Grant. As you can see, the game, just like the rest of the Chromium project, is fully open source. In this post, I want to show you how to use the Gamepad API.
Use the Gamepad API
Feature detection and browser support
The Gamepad API has universally great browser support across both desktop and mobile. You can detect if the Gamepad API is supported using the following snippet:
if ('getGamepads' in navigator) {
// The API is supported!
}
How the browser represents a gamepad
The browser represents gamepads as Gamepad
objects. A Gamepad
has the following properties:
id
: An identification string for the gamepad. This string identifies the brand or style of connected gamepad device.displayId
: TheVRDisplay.displayId
of an associatedVRDisplay
(if relevant).index
: The index of the gamepad in the navigator.connected
: Indicates whether the gamepad is still connected to the system.hand
: An enum defining what hand the controller is being held in, or is most likely to be held in.timestamp
: The last time the data for this gamepad was updated.mapping
: The button and axes mapping in use for this device, either"standard"
or"xr-standard"
.pose
: AGamepadPose
object representing the pose information associated with a WebVR controller.axes
: An array of values for all axes of the gamepad, linearly normalized to the range of-1.0
–1.0
.buttons
: An array of button states for all buttons of the gamepad.
Note that buttons can be digital (pressed or not pressed) or analog (for example, 78% pressed). This
is why buttons are reported as GamepadButton
objects, with the following attributes:
pressed
: The pressed state of the button (true
if the button is pressed, andfalse
if it is not pressed.touched
: The touched state of the button. If the button is capable of detecting touch, this property istrue
if the button is being touched, andfalse
otherwise.value
: For buttons that have an analog sensor, this property represents the amount by which the button has been pressed, linearly normalized to the range of0.0
–1.0
.hapticActuators
: An array containingGamepadHapticActuator
objects, each of which represents haptic feedback hardware available on the controller.
One additional thing that you might encounter, depending on your browser and the gamepad you have,
is a vibrationActuator
property. It allows for two kinds rumble effects:
- Dual-Rumble: The haptic feedback effect generated by two eccentric rotating mass actuators, one in each grip of the gamepad.
- Trigger-Rumble: The haptic feedback effect generated by two independent motors, with one motor located in each of the gamepad's triggers.
The following schematic overview, taken straight from the spec, shows the mapping and the arrangement of the buttons and axes on a generic gamepad.
Notification when a gamepad gets connected
To learn when a gamepad is connected, listen for the gamepadconnected
event that triggers on the
window
object. When the user connects a gamepad, which can either happen using USB or using Bluetooth,
a GamepadEvent
is fired that has the gamepad's details in an aptly named gamepad
property.
In the following, you can see an example from an Xbox 360 controller that I had lying around (yes, I am into
retro gaming).
window.addEventListener('gamepadconnected', (event) => {
console.log('✅ 🎮 A gamepad was connected:', event.gamepad);
/*
gamepad: Gamepad
axes: (4) [0, 0, 0, 0]
buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
connected: true
id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
index: 0
mapping: "standard"
timestamp: 6563054.284999998
vibrationActuator: GamepadHapticActuator {type: "dual-rumble"}
*/
});
Notification when a gamepad gets disconnected
Being notified of gamepad disconnections happens analogously to the way connections are detected.
This time the app listens for the gamepaddisconnected
event. Note how in the following example
connected
is now false
when I unplug the Xbox 360 controller.
window.addEventListener('gamepaddisconnected', (event) => {
console.log('❌ 🎮 A gamepad was disconnected:', event.gamepad);
/*
gamepad: Gamepad
axes: (4) [0, 0, 0, 0]
buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
connected: false
id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
index: 0
mapping: "standard"
timestamp: 6563054.284999998
vibrationActuator: null
*/
});
The gamepad in your game loop
Getting ahold of a gamepad starts with a call to navigator.getGamepads()
, which returns an array
with Gamepad
items. The array in Chrome always has a fixed length of four items. If zero or less
than four gamepads are connected, an item may just be null
. Always be sure to check all items of
the array and be aware that gamepads "remember" their slot and may not always be present at the
first available slot.
// When no gamepads are connected:
navigator.getGamepads();
// (4) [null, null, null, null]
If one or several gamepads are connected, but navigator.getGamepads()
still reports null
items,
you may need to "wake" each gamepad by pressing any of its buttons. You can then poll the gamepad
states in your game loop as shown in the following code.
const pollGamepads = () => {
// Always call `navigator.getGamepads()` inside of
// the game loop, not outside.
const gamepads = navigator.getGamepads();
for (const gamepad of gamepads) {
// Disregard empty slots.
if (!gamepad) {
continue;
}
// Process the gamepad state.
console.log(gamepad);
}
// Call yourself upon the next animation frame.
// (Typically this happens every 60 times per second.)
window.requestAnimationFrame(pollGamepads);
};
// Kick off the initial game loop iteration.
pollGamepads();
The vibration actuator
The vibrationActuator
property returns a GamepadHapticActuator
object, which corresponds to a
configuration of motors or other actuators that can apply a force for the purposes of haptic
feedback. Haptic effects can be played by calling Gamepad.vibrationActuator.playEffect()
. The only
valid effect types are 'dual-rumble'
and 'trigger-rumble'
.
Supported rumble effects
if (gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
// Trigger rumble supported.
} else if (gamepad.vibrationActuator.effects.includes('dual-rumble')) {
// Dual rumble supported.
} else {
// Rumble effects aren't supported.
}
Dual rumble
Dual-rumble describes a haptic configuration with an eccentric rotating mass vibration motor in each handle of a standard gamepad. In this configuration, either motor is capable of vibrating the whole gamepad. The two masses are unequal so that the effects of each can be combined to create more complex haptic effects. Dual-rumble effects are defined by four parameters:
duration
: Sets the duration of the vibration effect in milliseconds.startDelay
: Sets the duration of the delay until the vibration is started.strongMagnitude
andweakMagnitude
: Set the vibration intensity levels for the heavier and lighter eccentric rotating mass motors, normalized to the range0.0
–1.0
.
// This assumes a `Gamepad` as the value of the `gamepad` variable.
const dualRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
if (!('vibrationActuator' in gamepad)) {
return;
}
gamepad.vibrationActuator.playEffect('dual-rumble', {
// Start delay in ms.
startDelay: delay,
// Duration in ms.
duration: duration,
// The magnitude of the weak actuator (between 0 and 1).
weakMagnitude: weak,
// The magnitude of the strong actuator (between 0 and 1).
strongMagnitude: strong,
});
};
Trigger rumble
Trigger rumble is the haptic feedback effect generated by two independent motors, with one motor located in each of the gamepad's triggers.
// This assumes a `Gamepad` as the value of the `gamepad` variable.
const triggerRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
if (!('vibrationActuator' in gamepad)) {
return;
}
// Feature detection.
if (!('effects' in gamepad.vibrationActuator) || !gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
return;
}
gamepad.vibrationActuator.playEffect('trigger-rumble', {
// Duration in ms.
duration: duration,
// The left trigger (between 0 and 1).
leftTrigger: leftTrigger,
// The right trigger (between 0 and 1).
rightTrigger: rightTrigger,
});
};
Integration with Permissions Policy
The Gamepad API spec defines a
policy-controlled feature identified by the
string "gamepad"
. Its default allowlist
is "self"
. A document's permissions policy determines
whether any content in that document is allowed to access navigator.getGamepads()
. If disabled in
any document, no content in the document will be allowed to use navigator.getGamepads()
, nor will
the gamepadconnected
and gamepaddisconnected
events fire.
<iframe src="index.html" allow="gamepad"></iframe>
Demo
A gamepad tester demo is embedded in the following example. The source code is available on Glitch. Try the demo by connecting a gamepad using USB or Bluetooth and pressing any of its buttons or moving any of its axis.
Bonus: play Chrome dino on web.dev
You can play Chrome dino with your gamepad on this
very site. The source code is available on GitHub.
Check out the gamepad polling implementation in
trex-runner.js
and note how it is emulating key presses.
For the Chrome dino gamepad demo to work, I have ripped out the Chrome dino game from the core Chromium project (updating an earlier effort by Arnelle Ballane), placed it on a standalone site, extended the existing gamepad API implementation by adding ducking and vibration effects, created a full screen mode, and Mehul Satardekar contributed a dark mode implementation. Happy gaming!
Useful links
Acknowledgements
This document was reviewed by François Beaufort and Joe Medley. The Gamepad API spec is edited by Steve Agoston, James Hollyer, and Matt Reynolds. The former spec editors are Brandon Jones, Scott Graham, and Ted Mielczarek. The Gamepad Extensions spec is edited by Brandon Jones. Hero image by Laura Torrent Puig.