Skip to content

Controllers

Joël Deffner edited this page Sep 3, 2026 · 1 revision

Controllers

Since v0.5.0. steam.controllers is the curated layer over ISteamInput: controller handles, action sets, action data, haptics, and the device callbacks. It is called controllers because the generated ISteamInput class already owns steam.input.

Steam Input does not give you buttons. It maps every controller, whatever the hardware, to the actions named in your app's action manifest, and the user rebinds them in the Steam UI. So the flow is fixed: init once, list for the connected controllers, actionSet and digitalAction / analogAction for the handles at startup, then runFrame plus digital / analog once per frame.

Nothing here is async. Every call reads the local Steam client. The Controllers instance is created lazily and cached on the Steam object. Every handle (controller, action set, action) is a 64-bit value, so a bigint; origins, colors and speeds are plain numbers.

The action-data calls were the last of the generator's skips to be bound. They work since 0.3.0, which is what makes this layer possible at all.

init and shutdown

init(explicitRunFrame?: boolean): boolean
shutdown(): boolean

init starts Steam Input and must run before anything else here; shutdown stops it and releases the controller handles.

explicitRunFrame (default false) decides who updates the action data. Pass true and the values only change when runFrame is called, which is what a game loop wants. Pass false and Steam updates on its own schedule.

import { init } from 'steamwand.js';

const steam = init({ appId: 480 });
steam.controllers.init(true);
console.log(steam.controllers.list());
steam.controllers.shutdown();
steam.close();

runFrame

runFrame(): void

Refreshes the action data for every controller. Call it once per frame when init(true) was used. Without it the values from digital, analog and motion never change, which is the usual reason a first Steam Input build reads nothing but zeroes.

list

list(): bigint[]

The input handles of the currently connected controllers, at most 16 (STEAM_INPUT_MAX_COUNT). Empty when none is connected.

type

type(handle: bigint): number

What kind of controller a handle belongs to, as an ESteamInputType: 0 unknown, 2 Xbox 360, 3 Xbox One, 5 PS4, 13 PS5, 14 Steam Deck, and more. Use it to pick a button-prompt style when you are not using the glyphs.

actionSet

actionSet(name: string): bigint

The handle of an action set, by the name in the action manifest. Handles never change while the app runs, so look them up once at startup.

Throws Error: steamwand: no action set named '<name>' in the action manifest when Steam returns 0n, which means the name is misspelled or the manifest is not the one Steam loaded.

digitalAction and analogAction

digitalAction(name: string): bigint
analogAction(name: string): bigint

The handle of a digital (on or off) or analog (stick, pad or trigger) action, by manifest name. Both throw Error: steamwand: no <kind> action named '<name>' in the action manifest for a name Steam does not know.

activateActionSet

activateActionSet(handle: bigint, actionSet: bigint): void

Makes one action set the active one for a controller. Only one is active at a time, which is how a game switches between, say, walking and driving. Steam has no result for this, so it cannot fail from JavaScript.

currentActionSet

currentActionSet(handle: bigint): bigint

The active action set of a controller, or 0n when none is active.

Action set layers

activateActionSetLayer(handle: bigint, layer: bigint): void
deactivateActionSetLayer(handle: bigint, layer: bigint): void
deactivateAllActionSetLayers(handle: bigint): void

A layer sits on top of the active action set and overrides only the actions it binds, leaving the rest alone. Layers stack, so activateActionSetLayer may be called more than once, and the two deactivate calls remove one layer or all of them. Layer handles come from actionSet like any other.

digital

digital(handle: bigint, action: bigint): DigitalAction

The current state of a digital action: state is whether it is pressed, active whether it is bound at all in the active action set. A false active makes state meaningless, so check it before reacting to a false press.

steam.controllers.init(true);
const [pad] = steam.controllers.list();
const fire = steam.controllers.digitalAction('fire');
steam.controllers.runFrame();
if (pad && steam.controllers.digital(pad, fire).state) console.log('firing');

analog

analog(handle: bigint, action: bigint): AnalogAction

The current state of an analog action: the two axes, the EInputSourceMode that says how to read them, and the same active flag. The scale depends on mode: -1 to 1 for a joystick, a delta for a mouse-like source.

motion

motion(handle: bigint): InputMotionData_t

The gyro and accelerometer of a controller: the rotation quaternion, the acceleration, and the angular velocity, as the raw generated struct. Only hardware with motion sensors (Steam Controller, Steam Deck, DualShock, Switch pads) reports anything; the rest return zeroes.

vibrate and vibrateExtended

vibrate(handle: bigint, leftSpeed: number, rightSpeed: number): void
vibrateExtended(
  handle: bigint,
  leftSpeed: number, rightSpeed: number,
  leftTriggerSpeed: number, rightTriggerSpeed: number,
): void

vibrate runs the two main rumble motors, 0 to 65535 each. The speeds hold until they are set again, so pass zeroes to stop. vibrateExtended adds the trigger motors, which only reach hardware that has them, for example the Xbox One and DualSense pads.

triggerHaptic

triggerHaptic(
  handle: bigint,
  location: number,
  intensity: number,
  gainDb?: number,
  otherIntensity?: number,
  otherGainDb?: number,
): void

Plays one haptic click on a controller's haptic speakers, which is Steam Controller and Steam Deck hardware only. location is an EControllerHapticLocation: 1 left, 2 right, 3 both. intensity is 0 to 255, gainDb a signed loudness offset normally between -25 and 6. The two other parameters apply to the second pad when location is both.

setLedColor

setLedColor(handle: bigint, r: number, g: number, b: number, flags?: number): void

Sets the color of a controller's LED, DualShock and DualSense only. flags is an ESteamInputLEDFlag: 1 sets the color (the default), 2 restores the user's default and ignores the color. Steam has no result, so it cannot fail from JavaScript.

showBindingPanel

showBindingPanel(handle: bigint): boolean

Opens the Steam overlay on the binding screen for a controller. It needs the overlay, so it returns false when the overlay is disabled or the app runs outside Steam.

glyphForOrigin and originName

glyphForOrigin(origin: number, size?: number): string
originName(origin: number): string

glyphForOrigin returns the absolute path of the PNG glyph for one input origin: the button picture to show in a prompt, matching whatever hardware the action is bound to right now. size is an ESteamInputGlyphSize: 0 small, 1 medium (the default), 2 large. originName returns the human readable name of the same origin, for example A Button, in the Steam client language.

Origins come from steam.input.GetDigitalActionOrigins and its analog twin, which this layer does not wrap. Both calls give an empty string for an origin Steam does not know.

enableDeviceCallbacks

enableDeviceCallbacks(): void

Turns the device callbacks on. Without this, none of the three subscriptions below ever fires, because Steam does not send SteamInputDeviceConnected_t, SteamInputDeviceDisconnected_t or SteamInputConfigurationLoaded_t until it is called. Steam has no way to turn them off again.

onConnected and onDisconnected

onConnected(listener: (handle: bigint) => void): () => void
onDisconnected(listener: (handle: bigint) => void): () => void

Subscribe to controllers appearing and going away, with the input handle of each, and return unsubscribe functions. Both need enableDeviceCallbacks first.

steam.controllers.init(true);
steam.controllers.enableDeviceCallbacks();
const off = steam.controllers.onConnected((handle) => console.log('pad', handle));
// later: off(); steam.controllers.shutdown();

onConfigurationLoaded

onConfigurationLoaded(
  listener: (event: SteamInputConfigurationLoaded_t) => void,
): () => void

Fires when Steam applies a binding configuration to a controller, which also happens on connect. The decoded struct says which device it was, who made the configuration, and whether it drives the Steam Input or the gamepad API. Passed through unchanged, because every field of it is worth having.

Types

DigitalAction

Returned by digital.

Field Type Meaning
state boolean True while the action is pressed.
active boolean True while the action is bound in the active action set. A false makes state meaningless.

AnalogAction

Returned by analog.

Field Type Meaning
x number Horizontal value. Range depends on mode.
y number Vertical value, same scale as x.
mode number EInputSourceMode: how to read x and y (joystick, trigger, mouse, and so on).
active boolean True while the action is bound. A false makes x and y meaningless.

InputMotionData_t and SteamInputConfigurationLoaded_t are the generated structs, unchanged. See Flat API.

Errors

Shape When
Error: steamwand: no action set named '<name>' in the action manifest actionSet got 0n back.
Error: steamwand: no digital action named '<name>' in the action manifest digitalAction got 0n back.
Error: steamwand: no analog action named '<name>' in the action manifest analogAction got 0n back.

Those three are the whole error surface. Every other method either returns a boolean Steam gave it or calls a function with no result, so a controller that ignores a rumble or an LED command reports nothing.

What this layer does not do

The rest of ISteamInput is on the raw generated steam.input:

  • The origin lookups that feed glyphForOrigin: GetDigitalActionOrigins, GetAnalogActionOrigins, GetActionOriginFromXboxOrigin, TranslateActionOrigin.
  • The name and glyph variants: GetGlyphSVGForActionOrigin, GetGlyphForActionOrigin_Legacy, GetStringForDigitalActionName, GetStringForAnalogActionName, GetStringForXboxOrigin and GetGlyphForXboxOrigin.
  • GetActiveActionSetLayers, which reads back the layer stack this layer only writes.
  • GetDeviceBindingRevision, GetRemotePlaySessionID, GetSessionInputConfigurationSettings, GetControllerForGamepadIndex and GetGamepadIndexForController.
  • SetInputActionManifestFilePath, for an app that ships its manifest outside the Steam depot.
  • Legacy_TriggerHapticPulse and Legacy_TriggerRepeatedHapticPulse, the Steam Controller pulse calls that predate TriggerSimpleHapticEvent.
  • BWaitForData, BNewDataAvailable and StopAnalogActionMomentum.
  • SetDualSenseTriggerEffect, the adaptive trigger control.
  • Everything on ISteamController, the deprecated interface Steam Input replaced. It is generated as steam.controller and should not be used in new code.

Flat API explains the calling convention.

Next: System for the Steam Deck check that usually sits next to this layer.

Clone this wiki locally