All articles

Tutorial

Using the Gamepad API in Android TWAs

September 26, 2026 · 7 min read

The performance of modern mobile web browsers has made HTML5 gaming a highly viable path for cross-platform developers. By leveraging a Trusted Web Activity (TWA), you can package your web-based game into a signed Android App Bundle (AAB) and publish it directly to the Google Play Store. To deliver a high-quality console experience, especially for Chromebooks, Android TV, and mobile device users with Bluetooth or USB controllers, implementing the Web Gamepad API is essential. Because TWAs run on top of the system's default browser engine, they pass hardware inputs directly from the Android operating system to your web app without requiring native Java or Kotlin bridges.

How Android Inputs Flow Into a TWA

When a user connects a physical controller to their Android device via Bluetooth or a USB OTG cable, the Android system handles the driver-level mapping. The input events are captured by the active window. In the case of a TWA, this window is backed by the system's custom tab provider, which is typically Google Chrome. Chrome automatically translates these native Android MotionEvent and KeyEvent inputs into the standard W3C Gamepad API specification, making physical inputs instantly accessible to your JavaScript application.

Detecting Gamepad Connections

The Gamepad API relies on window-level event listeners to notify your application when a controller is registered or disconnected. This is particularly useful for showing visual cues to the user, such as a controller icon or an in-game notification confirming the connection.

You can listen for these state changes using the following JavaScript pattern:

window.addEventListener("gamepadconnected", (event) => { const gp = event.gamepad; console.log("Gamepad connected at index: " + gp.index + " ID: " + gp.id); startGameLoop(); }); window.addEventListener("gamepaddisconnected", (event) => { console.log("Gamepad disconnected from index: " + event.gamepad.index); pauseGame(); });

The gamepadconnected event provides a Gamepad object. This object contains metadata about the physical device, including its unique ID, its index array position, the number of physical buttons available, and the number of axes representing analogue joysticks or directional pads.

Implementing a High-Performance Polling Loop

Unlike standard keyboard or touch events, the Gamepad API does not push continuous event triggers to your application when a button is held down or an analogue stick is moved. Instead, you must poll the state of the connected controllers inside your game's rendering loop. The industry standard approach is to use the requestAnimationFrame API to read input states immediately before each frame render.

The following template illustrates how to structure a low-latency polling loop:

let gameLoopRunning = false; function startGameLoop() { if (!gameLoopRunning) { gameLoopRunning = true; requestAnimationFrame(updateLoop); } } function updateLoop() { const gamepads = navigator.getGamepads ? navigator.getGamepads() : []; for (let i = 0; i < gamepads.length; i++) { const gp = gamepads[i]; if (gp) { processInput(gp); } } if (gameLoopRunning) { requestAnimationFrame(updateLoop); } } function processInput(gamepad) { if (gamepad.buttons[0].pressed) { // Primary action button (A on Xbox, Cross on PlayStation) jump(); } // Reading analog stick axis coordinates const xAxis = gamepad.axes[0]; // Left stick horizontal axis const yAxis = gamepad.axes[1]; // Left stick vertical axis moveCharacter(xAxis, yAxis); }

Note that navigator.getGamepads() returns a snapshot array of all controllers currently connected. It is critical to fetch this list fresh on every frame, as the property values update dynamically behind the scenes in the browser engine.

Mapping Android Controller Layouts

Most modern controllers adhere to the standard layout configuration defined by the W3C spec. This layout maps specific buttons to standardized array indices, regardless of whether the physical hardware is an Xbox controller, a PlayStation DualSense, or a third-party mobile gamepad controller clamp.

Array IndexPhysical Controller Button (Standard Layout)
0Primary Action (A / Cross)
1Secondary Action (B / Circle)
2Tertiary Action (X / Square)
3Quaternary Action (Y / Triangle)
4Left Bumper (L1)
5Right Bumper (R1)
6Left Trigger (L2 - value ranges from 0.0 to 1.0)
7Right Trigger (R2 - value ranges from 0.0 to 1.0)
12D-Pad Up
13D-Pad Down
14D-Pad Left
15D-Pad Right

For analogue joysticks, axis values generally range from -1.0 (fully left/up) to 1.0 (fully right/down). When processing analogue sticks, always apply a minor deadzone threshold (typically between 0.1 and 0.15) to prevent character drifting caused by slight physical wear in the controller hardware.

function applyDeadzone(value, threshold = 0.15) { return Math.abs(value) > threshold ? value : 0; }

Optimising Input Latency and Performance in TWAs

Publishing your HTML5 game to Google Play means competing directly with native C++ and Kotlin applications. To ensure your web game feels just as responsive as a native title, you must optimise your input handling strategy.

  • Avoid Garbage Collection Spikes: Do not instantiate new objects, arrays, or vector instances inside your requestAnimationFrame loop. Re-use global variables or pre-allocated objects to keep the memory profile flat and prevent micro-stuttering caused by JavaScript garbage collection.
  • Set Passive Touch Listeners: If your game also supports on-screen touch virtual joysticks alongside physical controllers, ensure your touch listeners are set to passive. This signals to Chrome that the main thread does not need to wait for touch events to resolve before rendering subsequent frames.
  • Apply CSS Touch Action: Set touch-action: none; on your game canvas to prevent default browser gestures, such as pull-to-refresh or double-tap zooming, from interfering with fast-paced gameplay inputs.
  • Verify Chrome Custom Tab Engine: Ensure your app is built on a modern TWA wrapper configuration that defaults to Google Chrome rather than older fallback WebViews, as the native input delegation in Chrome is significantly more optimized for frame-rate synchronization.

Testing Your Gamepad Implementation

You can test your Gamepad API integration before assembling your finalized TWA wrapper. Connect your target controller to an Android device, open Google Chrome, and navigate to your local development server or staging URL. Once the inputs work flawlessly within mobile Chrome, compile your signed Android Package (APK) or Android App Bundle (AAB). The TWA will inherit the exact same low-latency input pipeline from the underlying Chrome engine upon installation.

Ready to ship your Android app?

Paste your PWA URL, get a signed APK and a Google Play ready AAB in minutes.

Build my app