A controller can appear to work normally while still having problems that affect gameplay.
A stick may not return perfectly to the center. A trigger may fail to reach its full range. Wireless mode may produce less stable updates than a wired connection. A controller may also behave differently across browsers, operating systems, and connection modes.
Most online controller testers only visualize buttons and axes. That is useful, but it does not answer a more important question:
Is the controller actually healthy?
While building Controller Tester Online, I explored how far the browser Gamepad API can go beyond basic input visualization.
The result is a browser-based testing workflow that checks buttons, triggers, analog sticks, stick drift, circularity, deadzone behavior, update stability, and vibration support.
However, browser-based testing also has important limitations. In particular, browser measurements should not be confused with low-level USB hardware analysis.
This article explains what the Gamepad API can measure reliably, what it can only estimate, and how I approached building a more complete controller health check.
Reading a Controller in the Browser
The Gamepad API provides access to controllers connected to a user's device.
A basic implementation can start like this:
const gamepads = navigator.getGamepads();
const gamepad = gamepads[0];
if (gamepad) {
console.log(gamepad.id);
console.log(gamepad.buttons);
console.log(gamepad.axes);
}
The browser exposes several useful properties:
{
id,
index,
connected,
mapping,
buttons,
axes,
timestamp
}
For each button, the browser can usually provide:
{
pressed,
touched,
value
}
Digital buttons normally return values close to either 0 or 1.
Analog inputs, such as triggers, may return values across a range.
Analog sticks are usually represented as axes between approximately -1 and 1.
A typical layout might look like this:
const leftStickX = gamepad.axes[0];
const leftStickY = gamepad.axes[1];
const rightStickX = gamepad.axes[2];
const rightStickY = gamepad.axes[3];
The exact axis and button mapping can vary depending on the controller, driver, browser, operating system, and whether the controller uses the standard mapping.
Why the User Usually Has to Press a Button First
Browsers do not always expose a connected controller immediately.
In many cases, the user must first press a button or move a stick. This behavior helps prevent websites from silently identifying connected gaming devices without user interaction.
A controller testing page therefore needs a clear initial state, such as:
Connect your controller, then press any button to activate it.
The page can also listen for connection events:
window.addEventListener("gamepadconnected", (event) => {
console.log("Controller connected:", event.gamepad.id);
});
window.addEventListener("gamepaddisconnected", (event) => {
console.log("Controller disconnected:", event.gamepad.id);
});
However, connection events alone are not enough. The application still needs to read the latest controller state continuously.
A common approach is to use requestAnimationFrame:
function updateController() {
const gamepads = navigator.getGamepads();
const gamepad = gamepads[0];
if (gamepad) {
updateButtons(gamepad.buttons);
updateAxes(gamepad.axes);
}
requestAnimationFrame(updateController);
}
requestAnimationFrame(updateController);
This works well for visualization, but it is important to understand that the rendering loop is not necessarily the same as the controller's hardware polling loop.
That distinction becomes important later.
Raw Input Is Not a Health Check
Showing live button and stick values is useful, but it does not automatically tell the user whether the controller has a problem.
For example, a stick at rest might report:
X: 0.021
Y: -0.034
Does that mean the stick is drifting?
Not necessarily.
Small center deviations are common. Their impact depends on several factors:
- The size of the deviation
- Whether the value is stable or constantly moving
- The deadzone used by the game
- The controller model
- The connection mode
- Calibration and driver behavior
A better controller test therefore needs timed sampling rather than relying on a single reading.
Measuring Stick Drift
For a basic drift test, the user should place the controller on a stable surface and avoid touching the analog sticks.
The application can then sample both sticks for several seconds.
A simplified implementation might look like this:
function getStickMagnitude(x, y) {
return Math.sqrt(x * x + y * y);
}
function sampleStick(gamepad) {
const leftX = gamepad.axes[0] ?? 0;
const leftY = gamepad.axes[1] ?? 0;
const rightX = gamepad.axes[2] ?? 0;
const rightY = gamepad.axes[3] ?? 0;
return {
left: {
x: leftX,
y: leftY,
magnitude: getStickMagnitude(leftX, leftY),
},
right: {
x: rightX,
y: rightY,
magnitude: getStickMagnitude(rightX, rightY),
},
};
}
During the test, the tool can record:
- Average X and Y values
- Maximum distance from the center
- Variation over time
- The percentage of samples outside a chosen threshold
For example:
let maximumMagnitude = 0;
let magnitudeSum = 0;
let sampleCount = 0;
function addSample(x, y) {
const magnitude = Math.sqrt(x * x + y * y);
maximumMagnitude = Math.max(maximumMagnitude, magnitude);
magnitudeSum += magnitude;
sampleCount += 1;
}
function getAverageMagnitude() {
return sampleCount === 0 ? 0 : magnitudeSum / sampleCount;
}
A single short spike should not necessarily produce a severe warning. It is usually more useful to consider both the maximum deviation and the average behavior during the test.
The result can then be presented as practical guidance rather than an absolute diagnosis.
For example:
Left stick maximum center deviation: 3.2%
Suggested initial deadzone: approximately 4%–5%
The suggested deadzone should be slightly higher than the observed center deviation, while still remaining low enough to preserve responsiveness.
It is also important to avoid claiming that one threshold is correct for every controller.
Testing Stick Range and Circularity
A healthy analog stick should normally reach close to its expected outer range when rotated around the edge.
To test this, the user can rotate the stick slowly through a complete circle.
Each sample can be plotted on a two-dimensional coordinate system:
const point = {
x: gamepad.axes[0],
y: gamepad.axes[1],
};
A perfectly circular result would follow an ideal unit circle:
x² + y² = 1
Real controllers rarely produce a perfect circle.
Some may produce:
- Slightly square output
- Restricted diagonal movement
- Uneven maximum range
- Different results in different directions
- Values that exceed the expected unit circle
- Incomplete outer-edge coverage
The distance from the center can be calculated using:
const radius = Math.sqrt(x * x + y * y);
This makes it possible to inspect whether the stick consistently reaches the outer range.
However, circularity should not be treated as a simple pass-or-fail score.
A controller can have imperfect circularity and still perform normally in games. Different controller firmware and calibration systems may intentionally shape analog output differently.
The most useful result is usually a visual trace combined with several measurements:
- Minimum outer radius
- Maximum outer radius
- Average radius
- Directional consistency
- Percentage of expected outer-edge coverage
Checking Trigger Range
Triggers can also develop problems.
A trigger may:
- Fail to return completely to zero
- Fail to reach its maximum value
- Move inconsistently
- Produce unstable values
- Have a large inactive area at the beginning of travel
A trigger test should record both its resting value and maximum value.
For example:
let minimumValue = 1;
let maximumValue = 0;
function updateTrigger(value) {
minimumValue = Math.min(minimumValue, value);
maximumValue = Math.max(maximumValue, value);
}
The user can then be asked to release and fully press each trigger.
A result might look like:
Left trigger resting value: 0.000
Left trigger maximum value: 0.984
This is more informative than only showing a moving progress bar.
Estimating Update Rate in a Browser
Polling rate is one of the most easily misunderstood measurements in browser-based controller testing.
A website does not have the same low-level access as a USB protocol analyzer or native hardware testing application.
The browser may expose a timestamp for the gamepad state:
const timestamp = gamepad.timestamp;
By observing changes in this timestamp, an application can estimate how frequently the browser receives new controller state.
A basic calculation might look like this:
const interval = currentTimestamp - previousTimestamp;
if (interval > 0) {
const estimatedRate = 1000 / interval;
}
For example, an interval of approximately 4 milliseconds would correspond to:
1000 / 4 = 250 Hz
However, this should be described as a browser-observed estimate.
Several layers can influence the result:
- Controller hardware
- USB, Bluetooth, or wireless receiver
- Device firmware
- Operating-system input stack
- Driver behavior
- Browser implementation
- Page visibility
- Rendering performance
- JavaScript scheduling
- Power-saving features
The browser may also reuse state between frames or expose timestamps differently across platforms.
Therefore, a browser tool should not claim:
Your controller's exact hardware polling rate is 500 Hz.
A more accurate description is:
The browser observed controller updates at an estimated average rate of approximately 500 Hz during this sample.
That wording matters.
Polling Rate Versus Rendering Rate
It is also important not to calculate polling rate only from requestAnimationFrame.
Most displays run at 60 Hz, 120 Hz, 144 Hz, or another fixed refresh rate. If controller state is checked only once per animation frame, the measurement may be limited by the page's rendering frequency.
For example, a 60 Hz rendering loop runs approximately once every:
1000 / 60 = 16.67 ms
A controller may update several times within that interval, but the JavaScript code may only observe the latest state.
This means:
requestAnimationFrame(() => {
// This loop frequency is not automatically the controller polling rate.
});
For browser testing, update-rate measurements are most useful for relative comparisons.
For example:
- Wired mode versus Bluetooth mode
- Bluetooth mode versus a 2.4 GHz receiver
- One USB port versus another
- Stable updates versus large irregular gaps
- Different browsers on the same computer
The result should be interpreted as a practical browser-level signal rather than laboratory-grade hardware data.
Measuring Jitter
Average update rate does not tell the whole story.
Two controllers can have the same average estimated rate while producing very different timing consistency.
Suppose one controller produces intervals like:
4.0 ms
4.1 ms
3.9 ms
4.0 ms
Another might produce:
1.0 ms
8.0 ms
2.0 ms
5.0 ms
Both sets can produce a similar average, but the second is much less consistent.
A useful test should therefore record:
- Average interval
- Minimum interval
- Maximum interval
- Standard deviation
- Large timing spikes
- Distribution of intervals
A simplified standard-deviation calculation might look like this:
function standardDeviation(values) {
if (values.length === 0) return 0;
const average =
values.reduce((sum, value) => sum + value, 0) / values.length;
const variance =
values.reduce((sum, value) => {
const difference = value - average;
return sum + difference * difference;
}, 0) / values.length;
return Math.sqrt(variance);
}
This measurement can help identify unstable browser-observed updates, although it still cannot reveal exactly where in the hardware-to-browser pipeline the instability originated.
Detecting Vibration Support
Some browsers and controllers expose a vibration actuator through the Gamepad API.
A capability check may look like this:
const actuator = gamepad.vibrationActuator;
if (actuator) {
console.log("Vibration actuator detected");
}
A short vibration test might use:
await gamepad.vibrationActuator.playEffect("dual-rumble", {
duration: 500,
strongMagnitude: 0.8,
weakMagnitude: 0.5,
});
However, the availability of the API does not guarantee that vibration will work in every environment.
Support may depend on:
- The browser
- The operating system
- The controller
- The connection method
- Browser permissions
- Driver support
For this reason, a browser tool should distinguish between:
- A vibration interface being exposed
- A vibration command being accepted
- The user confirming that physical vibration occurred
Capability detection alone is not always enough.
Keeping Controller Tests Local
A controller testing tool does not need to upload input data to a server.
Button states, axes, timestamps, drift samples, and test results can all be processed locally in JavaScript.
This provides several benefits:
- Immediate feedback
- No account requirement
- Lower latency
- Better privacy
- Easier testing across devices
- No need to store controller identifiers or input history
The report can also be generated locally from the completed tests.
For a tool like this, browser-side processing is not just convenient. It is usually the most appropriate architecture.
Building a Complete Testing Workflow
Instead of presenting every metric at once, I organized the testing process into a sequence.
A typical workflow is:
- Connect and activate the controller
- Check the detected controller name and mapping
- Test every button
- Test both triggers
- Move both sticks through their full range
- Leave the sticks untouched for a drift test
- Rotate each stick for a circularity test
- Sample browser-observed update intervals
- Check vibration support
- Generate a summary report
This makes the process easier for non-technical users while still exposing detailed measurements for developers and advanced users.
The live implementation is available here:
The tests run inside the browser, and controller input data is processed locally.
What Browser Testing Can Tell You
A browser-based controller test can be useful for identifying:
- Buttons that do not register
- Buttons that remain active after release
- Triggers with incomplete travel
- Analog sticks that do not return near the center
- Significant stick drift
- Restricted stick range
- Uneven directional output
- Irregular browser-observed update timing
- Differences between connection modes
- Whether vibration support is available to the browser
These tests are also useful when buying or selling a used controller, comparing connection modes, troubleshooting input issues, or checking a controller before starting a game.
What Browser Testing Cannot Prove
A browser test cannot always determine:
- The exact USB hardware polling rate
- The precise source of input latency
- Whether a problem comes from firmware, drivers, the browser, or the operating system
- How every game applies its own deadzone and response curve
- Whether a controller meets laboratory calibration standards
- Whether a wireless timing spike came from interference or software scheduling
Browser measurements should therefore be treated as diagnostic evidence, not absolute hardware certification.
Lessons From Building the Tool
The main lesson was that visualization is easy, but interpretation is difficult.
Displaying an axis value is straightforward.
Explaining whether that value indicates a meaningful problem requires:
- Multiple samples
- Clear testing instructions
- Appropriate thresholds
- Transparent limitations
- Results that avoid false precision
The same applies to polling rate.
A number such as 250 Hz appears precise, but the surrounding measurement method matters more than the number itself.
A trustworthy testing tool should clearly separate:
- Direct values reported by the Gamepad API
- Calculations based on timed samples
- Browser-level estimates
- User-confirmed hardware behavior
That distinction helps users understand what the result actually means.
Feedback From Other Developers
Controller behavior varies significantly across devices and platforms, so broader testing is useful.
I would be interested to know:
- Which controllers produce unusual button or axis mappings?
- Do you see different results in Chrome, Edge, Firefox, or Safari?
- How different are your wired, Bluetooth, and 2.4 GHz results?
- Does vibration work correctly on your operating system?
- Are there additional controller health metrics that would be useful?
You can test your controller at Controller Tester Online and share your browser, operating system, controller model, and connection method in the comments.
Disclosure: AI was used to help improve the English wording and article structure. The testing methodology, implementation details, and technical conclusions were reviewed by the author.
Top comments (0)