Pages read game controllers through the standard Gamepad API. The library never polls the OS— you describe each controller, report connections and disconnections, and forward stick and button changes as they happen.

AppCore doesn't forward controllers either, so every application manages gamepad input this way. If you haven't created a renderer yet, see [Integrating into a Game Engine](/docs/2.0/integrating-into-a-game-engine).

## Describing a Controller

To describe a controller, call `Renderer::SetGamepadDetails()` with its slot index, a name string, and its axis and button counts.

Controllers belong to the `Renderer`, not an individual `View`.

### Controller Slots and Bounds

The slot index determines the controller's position in the page's `navigator.getGamepads()` array. Always number controllers starting from 0. If you assign a controller to slot 1 without registering slot 0, slot 0 reads `null`.

The device ID appears on the page as `gamepad.id`. Later button and axis events must stay within the declared counts— any index outside those counts is ignored.

If a controller's layout or name changes, call `Renderer::SetGamepadDetails()` again for that slot. Disconnect and reconnect the controller to apply the update— pages keep seeing the old description while the controller stays connected.

## Connecting and Disconnecting

To connect or disconnect a controller, dispatch a [`GamepadEvent`](/api/cpp/2_0_0/classultralight_1_1_gamepad_event.html) to `Renderer::FireGamepadEvent()`.

Setting the event type to `GamepadEvent::kType_GamepadConnected` makes the controller visible to pages. The page's `window` receives a `gamepadconnected` event immediately, without waiting for the user to press a button first.

When a controller is unplugged, fire another event with `GamepadEvent::kType_GamepadDisconnected`. The page's `window` receives a `gamepaddisconnected` event, and that slot in `navigator.getGamepads()` returns to `null`.

> 📘 Registration Order
>
> Describe a controller first, fire its connection event second, and then forward input changes. Connecting a slot that was never described has no effect.

## Forwarding Sticks and Buttons

To forward stick movement and button presses, dispatch a [`GamepadAxisEvent`](/api/cpp/2_0_0/classultralight_1_1_gamepad_axis_event.html) to `Renderer::FireGamepadAxisEvent()` and a [`GamepadButtonEvent`](/api/cpp/2_0_0/classultralight_1_1_gamepad_button_event.html) to `Renderer::FireGamepadButtonEvent()`.

```cpp
#include <Ultralight/Ultralight.h>

using namespace ultralight;

RefPtr<Renderer> renderer;

///
/// Called by our engine when a controller is plugged in.
///
void OnControllerConnected(uint32_t slot, const char* name, uint32_t axes,
                           uint32_t buttons) {
  ///
  /// Describe the controller first (its name shows up as gamepad.id).
  ///
  renderer->SetGamepadDetails(slot, name, axes, buttons);

  ///
  /// Now tell pages it's connected.
  ///
  GamepadEvent evt;
  evt.type = GamepadEvent::kType_GamepadConnected;
  evt.index = slot;
  renderer->FireGamepadEvent(evt);
}

///
/// Called by our engine whenever a stick or trigger moves.
///
void OnControllerAxis(uint32_t slot, uint32_t axis, double value) {
  GamepadAxisEvent evt;
  evt.index = slot;
  evt.axis_index = axis;
  evt.value = value; // Normalized to [-1.0, 1.0].
  renderer->FireGamepadAxisEvent(evt);
}

///
/// Called by our engine whenever a button's value changes.
///
void OnControllerButton(uint32_t slot, uint32_t button, double value) {
  GamepadButtonEvent evt;
  evt.index = slot;
  evt.button_index = button;
  evt.value = value; // 0.0 is released, 1.0 is fully pressed.
  renderer->FireGamepadButtonEvent(evt);
}
```

Pages treat any reading greater than zero as pressed— analog triggers can send fractional values to represent partial presses.

> 🚧 Call on the Renderer's Thread
>
> All gamepad calls must run on the Renderer's thread. If you read controllers on a background thread, forward each change with `Renderer::PostTask()`. See [Updating and Rendering](/docs/2.0/updating-and-rendering).

## Reading Controllers on the Page

JavaScript reads controller state by polling `navigator.getGamepads()` inside an animation loop.

```js
function pollControllers() {
  for (const pad of navigator.getGamepads()) {
    if (!pad)
      continue;

    // Axis values run from -1.0 to 1.0. Each button has a 'pressed' flag
    // and a 'value' from 0.0 to 1.0.
    if (pad.buttons[0].pressed)
      console.log(pad.id + ' says: Ultralight rocks! (axis 0 is ' +
                  pad.axes[0] + ')');
  }

  requestAnimationFrame(pollControllers);
}

requestAnimationFrame(pollControllers);
```

Each controller object exposes its `id`, an `axes` array, and a `buttons` array where each button provides a boolean `pressed` flag and a numeric `value`. For details on standard browser properties, see the MDN [Gamepad API](https://developer.mozilla.org/en-US/docs/Web/API/Gamepad) reference.

Gamepad state is process-wide— every page across every `View` sees every controller you forward. This differs from keyboard and mouse events, which reach only the specific `View` they are fired on. See [Keyboard Input](/docs/2.0/keyboard-input).
