loading
Note:
Press a button first For privacy, browsers hide connected controllers from a page until the user has interacted with one. Until then GetGamepads returns an empty array even with a pad plugged in - that is not a bug, and the fix is to ask the user to press a button.

Support check

IsSupported

Returns true when the runtime exposes navigator.getGamepads. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.

C#
@inject Bit.Butil.Gamepad gamepad

var supported = await gamepad.IsSupported();
Live sample
support check output
Results will appear here when you interact with the samples.

Read a snapshot

GetGamepads

A point-in-time read of every connected pad. Axes are floats from -1 to 1; buttons carry an analogue Value as well as Pressed and Touched, because triggers on a standard-mapping pad are buttons rather than axes. Mapping is 'standard' when the browser recognised the layout - otherwise the indices are device-specific.

C#
var pads = await gamepad.GetGamepads();

foreach (var pad in pads)
{
    // pad.Mapping == "standard": axes 0/1 are the left stick, 2/3 the right
    var leftX = pad.Axes.ElementAtOrDefault(0);
    var aPressed = pad.Buttons.ElementAtOrDefault(0)?.Pressed ?? false;
    var trigger = pad.Buttons.ElementAtOrDefault(7)?.Value ?? 0;   // analogue
}
Live sample
snapshot output
Results will appear here when you interact with the samples.

Watch for input

SubscribeChanges

Gamepads have no input events - polling is the only way to read them. SubscribeChanges polls on requestAnimationFrame (which also means it stops while the tab is hidden) and only crosses into .NET when the state actually differs, at most once per minIntervalMs. Analogue sticks jitter constantly, so that floor is what keeps a controller sitting still from calling into C# sixty times a second.

C#
private ButilSubscription? _watch;

_watch = await gamepad.SubscribeChanges(
    pads => InvokeAsync(() =>
    {
        _live = pads;
        StateHasChanged();
    }),
    minIntervalMs: 50);

// later:
await _watch.DisposeAsync();
Live sample
Live Not watching.
watch output
Results will appear here when you interact with the samples.

Connect and disconnect

SubscribeConnection

Fires when a pad is plugged in or unplugged. Both callbacks get the full list of connected pads as it is after the change, not just the one that changed, so you can rebind your UI from a single argument.

C#
_connection = await gamepad.SubscribeConnection(
    onConnected: pads => InvokeAsync(() => { /* ... */ }),
    onDisconnected: pads => InvokeAsync(() => { /* ... */ }));
Live sample
connection output
Results will appear here when you interact with the samples.

Rumble

Vibrate / ResetVibration

Plays a dual-rumble effect on a pad's two motors: strongMagnitude drives the low-frequency (heavy) one, weakMagnitude the high-frequency (light) one, both from 0 to 1. Returns false when the pad is gone or has no vibration actuator - check HasVibration on a snapshot first.

C#
var played = await gamepad.Vibrate(
    index: 0,
    durationMs: 300,
    strongMagnitude: 1.0,
    weakMagnitude: 0.4);

// cut an effect short:
await gamepad.ResetVibration(0);
Live sample
rumble output
Results will appear here when you interact with the samples.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes navigator.getGamepads. Returns default (false) during prerender/SSR instead of throwing.
GetGamepads
ValueTask<GamepadState[]> GetGamepads()
A snapshot of every connected pad. Empty until the user has pressed a button on one.
SubscribeConnection
ValueTask<ButilSubscription> SubscribeConnection(Action<GamepadState[]>? onConnected = null, Action<GamepadState[]>? onDisconnected = null)
Watches pads being plugged in and unplugged. Both callbacks receive the full list after the change.
SubscribeChanges
ValueTask<ButilSubscription> SubscribeChanges(Action<GamepadState[]> handler, int minIntervalMs = 50)
Polls in the browser's frame loop and calls back only when the state changed, at most once per minIntervalMs. Pass 0 for every frame.
Vibrate
ValueTask<bool> Vibrate(int index, int durationMs = 200, double strongMagnitude = 1, double weakMagnitude = 1, int startDelayMs = 0)
Plays a dual-rumble effect. Magnitudes are clamped to 0-1. False when the pad is gone or has no actuator.
ResetVibration
ValueTask ResetVibration(int index)
Stops any haptic effect currently playing on that pad.
DisposeAsync
ValueTask DisposeAsync()
On scope/circuit teardown, detaches any listener whose subscription was never disposed.
GamepadState
Index, Id, Connected, Mapping, Timestamp, Axes, Buttons, HasVibration
A snapshot of one controller. Mapping is 'standard' when the browser recognised the layout.
GamepadButton
Pressed, Touched, Value
One button. Value is 0-1 - digital buttons only report 0 or 1, analogue triggers the whole range.
An unhandled error has occurred. Reload 🗙