Skip to content

Commit 9d44caa

Browse files
Update the gamepadrawinputchanged event explainer (post-2025)
1 parent 6d8ef52 commit 9d44caa

File tree

1 file changed

+96
-106
lines changed

1 file changed

+96
-106
lines changed

GamepadEventDrivenInputAPI/explainer.md

Lines changed: 96 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
## Participate
1010
- [Should fire events instead of using passive model #4](https://github.com/w3c/gamepad/issues/4)
11-
- [Need to spec liveness of Gamepad objects #8](https://github.com/w3c/gamepad/issues/8)
1211

1312
## Status of this document
1413

@@ -20,19 +19,18 @@ This document is a starting point for engaging the community and standards bodie
2019

2120
## Table of contents
2221

23-
1. [Introduction](#introduction)
22+
1. [Introduction](#introduction)
2423
2. [Definitions](#definitions)
25-
2. [User-facing problem](#user-facing-problem)
26-
3. [Proposed approach](#proposed-approach)
27-
4. [Example `rawgamepadinputchange` event](#example-rawgamepadinputchange-event)
28-
5. [Goals](#goals)
29-
6. [Non-goals](#non-goals)
30-
7. [Developer code sample](#developer-code-sample)
24+
3. [User-facing problem](#user-facing-problem)
25+
4. [Goals](#goals)
26+
5. [Non-goals](#non-goals)
27+
6. [Proposed approach](#proposed-approach)
28+
7. [Examples](#examples)
3129
8. [Alternatives considered](#alternatives-considered)
3230
9. [Accessibility, privacy, and security considerations](#accessibility-privacy-and-security-considerations)
3331
10. [Stakeholder feedback / opposition](#stakeholder-feedback--opposition)
3432
11. [References & acknowledgements](#references--acknowledgements)
35-
12. [Appendix: proposed WebIDL](#appendix-proposed-webidl)
33+
12. [Appendix: proposed WebIDL](#appendix-proposed-webidl)
3634

3735
## Introduction
3836

@@ -45,8 +43,8 @@ This proposal builds on earlier work by Chromium engineers, which explored event
4543
### Input frame:
4644
Each input frame refers to a single timestamped update of a gamepad’s state, typically derived from a HID (Human Interface Device) report, including all button and axis values at that moment in time.
4745

48-
### RawGamepadInputChange event:
49-
An event that represents a snapshot of a gamepad’s state at the moment a new input frame is received from the gamepad device. Each event corresponds to a full input report (e.g., a HID report) and contains the complete state of all buttons, axes. This event enables applications to react to input in a timely, event-driven manner, as an alternative to polling via navigator.getGamepads().
46+
### The `gamepadrawinputchanged` event:
47+
An event that represents a snapshot of a gamepad’s state at the moment a new input frame is received from the gamepad device. Each event corresponds to a full input report (e.g., a HID report) and contains the complete state of all buttons and axes. This event enables applications to react to input in a timely, event-driven manner, as an alternative to polling via `navigator.getGamepads()`.
5048

5149
## User-facing problem
5250

@@ -81,8 +79,8 @@ window.addEventListener('gamepadconnected', () => {
8179
});
8280
```
8381
#### Key points:
84-
- navigator.getGamepads() returns a snapshot of all connected gamepads.
85-
- The polling loop is driven by `requestAnimationFrame`, typically around 60Hz (matching display refresh rate), which is much lower than the internal OS poll rate (eg., 250Hz). This mismatch can result in missed input updates, making the 60Hz rate insufficient for latency-critical applications like cloud gaming.
82+
- `navigator.getGamepads()` returns a snapshot of all connected gamepads.
83+
- The polling loop is driven by `requestAnimationFrame()`, typically around 60Hz (matching display refresh rate), which is much lower than the internal OS poll rate (eg., 250Hz). This mismatch can result in missed input updates, making the 60Hz rate insufficient for latency-critical applications like cloud gaming.
8684

8785
## Goals
8886

@@ -94,38 +92,42 @@ Reduce input latency by moving away from constant polling and introducing event-
9492

9593
- Additionally, this proposal does not currently address input alignment or event coalescing. Prior work on high-frequency input APIs, particularly the Pointer Events API has demonstrated the importance of these mechanisms for latency-sensitive use cases. For instance, the [`pointerrawupdate`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerrawupdate_event) event was introduced to provide low-latency input delivery, and it is complemented by the [`getCoalescedEvents()`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents) method, which exposes intermediate pointer updates that occur between animation frames. Together, these features help align input processing with rendering, improving visual smoothness and reducing jitter.
9694

97-
In contrast, this proposal for `rawgamepadinputchange` intentionally omits alignment and coalescing in its initial design. At this stage, we've intentionally scoped this proposal to deliver immediate, per-HID-report events without adding alignment or coalescing mechanisms. This is both to reduce complexity up front and to validate the value of the raw event model for latency-sensitive use cases.
95+
In contrast, this proposal for `gamepadrawinputchanged` intentionally omits alignment and coalescing in its initial design. At this stage, we've intentionally scoped this proposal to deliver immediate, per-HID-report events without adding alignment or coalescing mechanisms. This is both to reduce complexity up front and to validate the value of the raw event model for latency-sensitive use cases.
9896

9997
That said, we recognize that high-frequency gamepad inputs could eventually require similar treatment to pointer events. This proposal is intended as a foundational step, and we explicitly leave room for future evolution. For further background, we recommend reviewing [prior discussions on event-driven gamepad APIs](https://github.com/w3c/gamepad/issues/4#issuecomment-894460031).
10098

10199
## Proposed approach
102-
### `rawgamepadinputchange` event
103-
To address the challenges of input latency, this proposal introduces a new event-driven mechanism: the `rawgamepadinputchange` event. This event fires directly on the [Gamepad](https://w3c.github.io/gamepad/#dom-gamepad) object and delivers real-time updates for each input frame, eliminating the need for high-frequency polling. The `rawgamepadinputchange` event includes detailed information about the state of the gamepad at the moment of change.
100+
101+
To address the challenges of input latency, this proposal introduces a new event-driven mechanism: the `gamepadrawinputchanged` event. This event fires directly on the window global object. The `gamepadrawinputchanged` event includes detailed information about the state of the gamepad at the moment of change.
104102

105103
### Event properties
104+
- `gamepad`: A read-only [Gamepad](https://developer.mozilla.org/en-US/docs/Web/API/Gamepad) object that is a snapshot of the gamepad’s state at the moment the input was received. It includes all axes, buttons, ID, index, and timestamp.
105+
106106
- `axesChanged` and `buttonsValueChanged`: Arrays of indices indicating which axes or button values changed since the last event.
107107

108108
- `buttonsPressed` and `buttonsReleased`: Indices of buttons whose pressed state transitioned (from pressed to released or vice versa).
109109

110-
- `gamepadSnapshot`: A frozen (read-only) snapshot of the gamepad’s state at the moment the input was received. It includes all axes, buttons, ID, index, and timestamp, and does not update after the event is dispatched.
110+
- `touchesChanged` : An array of indices indicating which touch-sensitive controls changed since the last input frame.
111+
- Some modern controllers include capacitive or touch-sensitive surfaces (e.g., DualShock 4 touchpad, Steam Controller trackpads). Each index in `touchesChanged` corresponds to an entry in the `gamepad.touches` array and reports which touch points or surfaces changed state (position, or touch presence).
111112

112-
These properties, `axesChanged`, `buttonsPressed`, `buttonsReleased`, and `buttonsValueChanged` properties are arrays of indices and follow the same indentification model as the [Gamepad.axes](https://w3c.github.io/gamepad/#dom-gamepad-axes) and [Gamepad.buttons](https://w3c.github.io/gamepad/#dom-gamepad-buttons) arrays.
113+
These properties, `axesChanged`, `buttonsPressed`, `buttonsReleased`, `buttonsValueChanged` and ` touchesChanged` properties are arrays of indices and follow the same identification model as the [Gamepad.axes](https://w3c.github.io/gamepad/#dom-gamepad-axes) and [Gamepad.buttons](https://w3c.github.io/gamepad/#dom-gamepad-buttons) arrays.
113114

114-
### Event behavior
115-
Dispatched on the Gamepad Object: The rawgamepadinputchange event is dispatched on the Gamepad object that experienced the input change. This Gamepad instance is accessible via the event's [`target`](https://developer.mozilla.org/en-US/docs/Web/API/Event/target) property and represents a live object that reflects the current state of the device.
115+
### Event timing
116116

117-
Real-Time Updates: A new rawgamepadinputchange event is dispatched for every gamepad input state change, without delay or coalescing. This enables latency-sensitive applications, such as rhythm games, cloud gaming, or real-time multiplayer scenarios, to respond immediately and accurately to input.
117+
A new `gamepadrawinputchanged` event is dispatched for every gamepad input state change, without delay or coalescing. This enables latency-sensitive applications, such as rhythm games, cloud gaming, or real-time multiplayer scenarios, to respond immediately and accurately to input
118118

119-
Gamepad Snapshot: The event also provides a `gamepadSnapshot` property which captures the input state at the exact time the event was generated - corresponding to the moment indicated by the HID input report's timestamp. This ensures that applications can reliably determine the exact state that triggered the event, even if the live object (`event.target`) has changed by the time the event handler runs.
119+
## Examples
120120

121-
## Example `rawgamepadinputchange` event
121+
### `gamepadrawinputchanged` event data view
122+
123+
The example below shows the structure of a `gamepadrawinputchanged` event, including the gamepad state snapshot and the indices of changed inputs.
122124

123125
```js
124-
rawgamepadinputchange {
125-
type: "rawgamepadchange",
126+
gamepadrawinputchangedEventObject {
127+
type: "gamepadrawinputchanged",
126128

127129
// Snapshot of the gamepad's state at the moment the event was generated.
128-
gamepadSnapshot: Gamepad {
130+
gamepad: Gamepad {
129131
id: "Xbox Wireless Controller (STANDARD GAMEPAD Vendor: 045e Product: 02fd)",
130132
index: 0,
131133
connected: true,
@@ -141,75 +143,73 @@ rawgamepadinputchange {
141143
],
142144
// [left stick X, left stick Y, right stick X, right stick Y].
143145
axes: [0.25, -0.5, 0.0, 0.0],
146+
touches: [
147+
// Index 0 — finger touching at position (0.42, 0.33).
148+
{
149+
touchId: 0,
150+
surfaceId: 0,
151+
position: Float32Array [0.42, 0.33],
152+
surfaceDimensions: Uint32Array [1920, 1080]
153+
},
154+
...
155+
],
144156
timestamp: 9123456.789
145157
},
146158

147159
// Left stick X and Y moved since last event.
148160
axesChanged: [0, 1],
149-
// Button index 0 was pressed and button index 1 released, button index 2 value changed.
161+
// Indices of buttons whose values changed.
150162
buttonsValueChanged: [0, 1, 2],
151-
// Button index 0 pressed.
163+
// Indices of buttons newly pressed.
152164
buttonsPressed: [0],
153-
// Button index 0 released.
154-
buttonsReleased: [1]
165+
// Indices of buttons newly released.
166+
buttonsReleased: [1],
167+
// Indices of touch points whose state changed.
168+
touchesChanged: [0]
155169
}
156170
```
157-
## Developer code sample
171+
172+
### Code sample
173+
174+
This example demonstrates how a web application can listen for `gamepadrawinputchanged` events and react to changes in gamepad input state.
158175

159176
```JS
160-
// Listen for when a gamepad is connected.
161-
window.ongamepadconnected = (connectEvent) => {
162-
163-
const connectedGamepads = navigator.getGamepads();
164-
165-
const gamepad = connectedGamepads[connectEvent.gamepad.index];
166-
167-
console.log(`Gamepad connected: ${gamepad.id} (index: ${gamepad.index})`);
168-
169-
// Listen for input changes on this gamepad.
170-
gamepad.onrawgamepadinputchange = (changeEvent) => {
171-
// Snapshot of the gamepad state at the time of the event.
172-
const snapshot = changeEvent.gamepadSnapshot;
173-
// Live gamepad object that continues to update.
174-
const liveGamepad = changeEvent.target;
175-
176-
for (const axisIndex of changeEvent.axesChanged) {
177-
const snapshotAxisValue = snapshot.axes[axisIndex];
178-
const liveAxisValue = liveGamepad.axes[axisIndex];
179-
console.log(`Axis ${axisIndex} on gamepad ${snapshot.index} changed to ${snapshotAxisValue} (live: ${liveAxisValue})`);
180-
}
181-
182-
// Analog button changes (ex: triggers).
183-
for (const buttonIndex of changeEvent.buttonsValueChanged) {
184-
const snapshotButtonValueChanged = snapshot.buttons[buttonIndex].value;
185-
const liveButtonsValueChanged = liveGamepad.buttons[buttonIndex].value;
186-
console.log(`button ${buttonIndex} on gamepad ${snapshot.index} changed to value ${snapshotButtonValueChanged} (live: ${liveButtonValueChanged})`);
187-
}
188-
189-
// Binary buttons that were pressed.
190-
for (const buttonIndex of changeEvent.buttonsPressed) {
191-
const snapshotButtonPressedValue = snapshot.buttons[buttonIndex].pressed;
192-
const liveButtonPressedValue = liveGamepad.buttons[buttonIndex].pressed;
193-
console.log(`button ${buttonIndex} on gamepad ${snapshot.index} changed to value ${snapshotButtonPressedValue} (live: ${liveButtonPressedValue}`);
194-
}
195-
196-
// Binary buttons that were released.
197-
for (const buttonIndex of changeEvent.buttonsReleased) {
198-
const snapshotButtonReleasedValue = snapshot.buttons[buttonIndex].released;
199-
const liveButtonReleasedValue = liveGamepad.buttons[buttonIndex].released;
200-
console.log(`button ${buttonIndex} on gamepad ${snapshot.index} changed to value ${snapshotButtonReleasedValue} (live: ${liveButtonReleasedValue}`);
201-
}
202-
};
203-
};
177+
function onRawInputChanged(event) {
178+
const snapshot = event.gamepad;
179+
console.log(`Received gamepadrawinputchanged event for gamepad ${snapshot.index} (${snapshot.id})`);
180+
181+
for (const axisIndex of event.axesChanged) {
182+
const axisValue = snapshot.axes[axisIndex];
183+
console.log(`Axis ${axisIndex} changed to value ${axisValue}`);
184+
}
185+
186+
for (const buttonIndex of event.buttonsValueChanged) {
187+
const buttonValue = snapshot.buttons[buttonIndex].value;
188+
console.log(`Button ${buttonIndex} value changed to ${buttonValue}`);
189+
}
190+
191+
for (const buttonIndex of event.buttonsPressed) {
192+
console.log(`Button ${buttonIndex} was pressed`);
193+
}
194+
195+
for (const buttonIndex of event.buttonsReleased) {
196+
console.log(`Button ${buttonIndex} was released`);
197+
}
198+
199+
for (const touchIndex of event.touchesChanged) {
200+
const touch = snapshot.touches[touchIndex];
201+
console.log(`Touch ${touchIndex} changed: id=${touch.touchId}, position=[${touch.position[0]}, ${touch.position[1]}]`);
202+
}
203+
}
204+
205+
window.addEventListener('gamepadrawinputchanged', onRawInputChanged);
204206
```
205207

206208
## Alternatives considered
207-
`gamepadinputchange` event: Similar to `rawgamepadinputchange` event but instead the `getCoalescedEvents()` method is used to return a sequence of events that have been coalesced (combined) together. While `gamepadinputchange` reduces the number of events by coalescing them, this approach introduces latency and may result in missed intermediate states, making it unsuitable for scenarios requiring immediate responsiveness. This event was proposed in the [Original Proposal](https://docs.google.com/document/d/1rnQ1gU0iwPXbO7OvKS6KO9gyfpSdSQvKhK9_OkzUuKE/edit?pli=1&tab=t.0).
209+
`gamepadinputchange` event: Similar to `gamepadrawinputchanged` event but instead the `getCoalescedEvents()` method is used to return a sequence of events that have been coalesced (combined) together. While `gamepadinputchange` reduces the number of events by coalescing them, this approach introduces latency and may result in missed intermediate states, making it unsuitable for scenarios requiring immediate responsiveness. This event was proposed in the [Original Proposal](https://docs.google.com/document/d/1rnQ1gU0iwPXbO7OvKS6KO9gyfpSdSQvKhK9_OkzUuKE/edit?pli=1&tab=t.0).
208210

209211
## Accessibility, privacy, and security considerations
210-
To prevent abuse and fingerprinting, a ["gamepad user gesture"](https://www.w3.org/TR/gamepad/#dfn-gamepad-user-gesture) will be required before `RawGamepadInputChange` events start firing (e.g., pressing a button).
211-
212-
Limit Persistent Tracking (fingerprinting): `rawgamepadinputchange` event will not expose any new state that is not already exposed by polling [Fingerprinting in Web](https://www.w3.org/TR/fingerprinting-guidance/).
212+
To prevent abuse and [fingerprinting](https://www.w3.org/TR/fingerprinting-guidance/), a ["gamepad user gesture"](https://www.w3.org/TR/gamepad/#dfn-gamepad-user-gesture) will be required before `gamepadrawinputchanged` events start firing (e.g., pressing a button). Moreover, `gamepadrawinputchanged` event will not expose any new state that is not already exposed by polling.
213213

214214
## Stakeholder feedback / opposition
215215
Firefox: No Signal
@@ -228,42 +228,32 @@ Thanks to the following contributors and prior work that influenced this proposa
228228

229229
Firefox’s experimental implementation: The [`GamepadAxisMoveEvent`](https://searchfox.org/mozilla-central/source/dom/webidl/GamepadAxisMoveEvent.webidl#9) and [`GamepadButtonEvent`](https://searchfox.org/mozilla-central/source/dom/webidl/GamepadButtonEvent.webidl) WebIDL files in Firefox defines an interface for axis movement and button press and release events, which were part of an experimental prototype implementation in Firefox for handling event-driven gamepad input.
230230

231-
Chromium Prior discussions on improving gamepad input handling - [Original Proposal](https://docs.google.com/document/d/1rnQ1gU0iwPXbO7OvKS6KO9gyfpSdSQvKhK9_OkzUuKE/edit?pli=1&tab=t.0).
231+
Chromium prior discussions on improving gamepad input handling - [Original Proposal](https://docs.google.com/document/d/1rnQ1gU0iwPXbO7OvKS6KO9gyfpSdSQvKhK9_OkzUuKE/edit?pli=1&tab=t.0).
232232

233233
Many thanks for valuable feedback and advice from:
234234
- [Steve Becker](https://github.com/SteveBeckerMSFT)
235235
- [Gabriel Brito](https://github.com/gabrielsanbrito)
236236
- [Matt Reynolds](https://github.com/nondebug)
237237

238238
## Appendix: proposed WebIDL
239+
### `GamepadRawInputChangeEvent` interface IDL, used for `gamepadrawinputchanged`.
239240
```JS
240-
[Exposed=Window]
241-
partial interface Gamepad : EventTarget {
242-
attribute EventHandler onrawgamepadinputchange;
243-
};
244-
245-
```
246-
### `RawGamepadInputChangeEvent` interface IDL, used for `rawgamepadinputchange`.
247-
```JS
248-
// Inherits `target` from Event, which refers to the live Gamepad.
249-
[Exposed=Window]
250-
interface RawGamepadInputChangeEvent : Event {
251-
constructor(DOMString type, optional RawGamepadInputChangeEventInit eventInitDict = {});
252-
253-
// Immutable snapshot of gamepad state at time of event dispatch.
254-
readonly attribute Gamepad gamepadSnapshot;
255-
256-
readonly attribute FrozenArray<unsigned long> axesChanged;
257-
readonly attribute FrozenArray<unsigned long> buttonsValueChanged;
258-
readonly attribute FrozenArray<unsigned long> buttonsPressed;
259-
readonly attribute FrozenArray<unsigned long> buttonsReleased;
241+
[
242+
Exposed=Window,
243+
] interface GamepadRawInputChangeEvent : GamepadEvent {
244+
constructor(DOMString type, optional GamepadRawInputChangeEventInit eventInitDict = {});
245+
readonly attribute FrozenArray<long> axesChanged;
246+
readonly attribute FrozenArray<long> buttonsValueChanged;
247+
readonly attribute FrozenArray<long> buttonsPressed;
248+
readonly attribute FrozenArray<long> buttonsReleased;
249+
readonly attribute FrozenArray<long> touchesChanged;
260250
};
261251

262-
dictionary RawGamepadInputChangeEventInit : EventInit {
263-
required Gamepad gamepadSnapshot;
264-
FrozenArray<unsigned long> axesChanged = [];
265-
FrozenArray<unsigned long> buttonsValueChanged = [];
266-
FrozenArray<unsigned long> buttonsPressed = [];
267-
FrozenArray<unsigned long> buttonsReleased = [];
252+
dictionary GamepadRawInputChangeEventInit : GamepadEventInit {
253+
FrozenArray<long> axesChanged;
254+
FrozenArray<long> buttonsValueChanged;
255+
FrozenArray<long> buttonsPressed;
256+
FrozenArray<long> buttonsReleased;
257+
FrozenArray<long> touchesChanged;
268258
};
269259
```

0 commit comments

Comments
 (0)