⚡ Simple keyboard & gamepad management for PixiJS
🎮 Handle keyboard, gamepads, and more! | 🚀 Real-time & event-driven APIs |
⚡ Highly-optimized for performance | 🧭 Built-in UI navigation |
🔮 Highly configurable (with sensible defaults) | 🪄 Supports input binding |
✅ Cross-platform & mobile-friendly [1] [2] [3] | 🌐 Automatic Intl layouts detection |
🍃 Zero dependencies & tree-shakeable | ✨ Supports PixiJS v8, v7, v6.3+ |
Handle device inputs with ease.
import { InputDevice, GamepadDevice } from "pixijs-input-devices"
// Set named binds
GamepadDevice.configureDefaultBinds({
jump: [ "Face1" ]
})
InputDevice.keyboard.configureBinds({
jump: [ "ArrowUp", "Space" ]
})
// Use binds
for (const device of InputDevice.devices) {
if (device.bindDown("jump")) // ...
}
// Event-driven
InputDevice.onBindDown("jump", ({ device }) => {
if (device.type === "gamepad") {
device.playVibration({ duration: 50 })
}
})
Everything you need to quickly integrate device management.
PixiJS Input Devices adds first-class support for input devices, and provides a simple, but powerful navigation manager that can enable devices to navigate existing pointer-based UIs.
The key concepts are:
- Devices: Any human interface device
- Binds: Custom, named input actions that can be triggered by assigned keys or buttons
- UINavigation: Navigation manager for non-pointer devices to navigate UIs
[!NOTE] See UINavigation API for more information.
Quick start guide.
1. Install the latest pixijs-input-devices
package:
# npm
npm install pixijs-input-devices -D
# yarn
yarn add pixijs-input-devices --dev
2. Register the update loop:
import { Ticker } from 'pixi.js'
import { InputDevice } from 'pixijs-input-devices'
Ticker.shared.add(() => InputDevice.update())
[!TIP] Input polling: In the context of a video game, you may want to put the input update at the start of your game event loop instead.
3. (Optional) enable the UINavigation API
import * as PIXI from 'pixi.js'
import { UINavigation, registerPixiJSNavigationMixin } from 'pixijs-input-devices'
const app = new PIXI.Application(/*…*/)
// enable the navigation API
UINavigation.configureWithRoot(app.stage)
registerPixiJSNavigationMixin(PIXI.Container)
✨ You are now ready to use inputs!
The InputDevice
singleton controls all device discovery.
InputDevice.keyboard // KeyboardDevice
InputDevice.gamepads // GamepadDevice[]
InputDevice.custom // Device[]
You can access all active/connected devices using .devices
:
for (const device of InputDevice.devices) { // …
The InputDevice
manager provides the following context capability properties:
Property | Type | Description |
---|---|---|
InputDevice.hasMouseLikePointer |
boolean |
Whether the context has a mouse/trackpad. |
InputDevice.isMobile |
boolean |
Whether the context is mobile capable. |
InputDevice.isTouchCapable |
boolean |
Whether the context is touchscreen capable. |
As well as shortcuts to connected devices:
Accessor | Type | Description |
---|---|---|
InputDevice.lastInteractedDevice |
Device? |
The most recently interacted device (or first if multiple). |
InputDevice.devices |
Device[] |
All active, connected devices. |
InputDevice.keyboard |
KeyboardDevice |
The global keyboard. |
InputDevice.gamepads |
GamepadDevice[] |
Connected gamepads. |
InputDevice.custom |
CustomDevice[] |
Any custom devices. |
Access global events directly through the manager:
InputDevice.on("deviceadded", ({ device }) => {
// new device was connected or became available
// do additional setup here, show a dialog, etc.
})
InputDevice.off("deviceadded") // stop listening
Event | Description | Payload |
---|---|---|
"deviceadded" |
{device} |
A device has been added. |
"deviceremoved" |
{device} |
A device has been removed. |
"lastdevicechanged" |
{device} |
The last interacted device has changed. |
You may also subscribe globally to named bind events:
InputDevice.onBindDown("my_custom_bind", (event) => {
// a bound input waas triggered
})
Unlike gamepads & custom devices, there is a single global keyboard device.
let keyboard = InputDevice.keyboard
if (keyboard.key.ControlLeft) { // …
[!NOTE] Detection: On mobiles/tablets the keyboard will not appear in
InputDevice.devices
until a keyboard is detected. Seekeyboard.detected
.
keyboard.layout // "AZERTY" | "JCUKEN" | "QWERTY" | "QWERTZ"
keyboard.getKeyLabel("KeyZ") // Я
[!NOTE] Layout support: Detects the "big four" (AZERTY, JCUKEN, QWERTY and QWERTZ). Almost every keyboard is one of these four (or a regional derivative – e.g. Hangeul, Kana). There is no built-in detection for specialist or esoteric layouts (e.g. Dvorak, Colemak, BÉPO).
The
keyboard.getKeyLabel(key)
uses the KeyboardLayoutMap API when available, before falling back to default AZERTY, JCUKEN, QWERTY or QWERTZ key values.
The keyboard layout is automatically detected from (in order):
- Browser API (browser support)
- Keypresses
- Browser Language
You can also manually force the layout:
// force layout
InputDevice.keyboard.layout = "JCUKEN"
InputDevice.keyboard.getKeyLabel("KeyW") // "Ц"
InputDevice.keyboard.layoutSource // "manual"
Event | Description | Payload |
---|---|---|
"layoutdetected" |
{layout,layoutSource,device} |
The keyboard layout ("QWERTY" , "QWERTZ" , "AZERTY" , or "JCUKEN" ) has been detected, either from the native API or from keypresses. |
"binddown" |
{name,event,keyCode,keyLabel,device} |
A named bind key was pressed. |
Key presses: | ||
"KeyA" |
{event,keyCode,keyLabel,device} |
The "KeyA" was pressed. |
"KeyB" |
{event,keyCode,keyLabel,device} |
The "KeyB" was pressed. |
"KeyC" |
{event,keyCode,keyLabel,device} |
The "KeyC" was pressed. |
… | … | … |
Gamepads are automatically detected via the browser API when first interacted with (read more).
Gamepad accessors are modelled around the "Standard Controller Layout":
const gamepad = InputDevice.gamepads[0];
if (gamepad.button.DpadDown)
{
// button pressed
}
if (gamepad.leftTrigger > 0.25)
{
// trigger pulled
}
if (gamepad.leftJoystick.x < -0.33)
{
// joystick moved
}
[!TIP] Special requirements? You can always access
gamepad.source
and reference the underlying API directly as needed.
Use the playVibration()
method to play a haptic vibration, in supported browsers.
gamepad.playVibration({
duration: 150,
weakMagnitude: 0.75,
strongMagnitude: 0.25,
// …
})
The gamepad buttons reference Standard Controller Layout:
Button # | GamepadCode | Description | Xbox Series X | Playstation 5 DualSense® | Nintendo Switch™ Pro |
---|---|---|---|---|---|
0 |
"Face1" |
Face Button 1 | A | Cross | B |
1 |
"Face2" |
Face Button 2 | B | Circle | A |
2 |
"Face3" |
Face Button 3 | X | Square | Y |
3 |
"Face4" |
Face Button 4 | Y | Triangle | X |
4 |
"LeftShoulder" |
Left Shoulder | LB | L1 | L |
5 |
"RightShoulder" |
Right Shoulder | RB | R1 | R |
6 |
"LeftTrigger" |
Left Trigger | LT | L2 | ZL |
7 |
"RightTrigger" |
Right Trigger | RT | R2 | ZR |
8 |
"Back" |
Back | View | Options | Minus |
9 |
"Start" |
Start | Menu | Select | Plus |
10 |
"LeftStickClick" |
Left Stick (Click) | LSB | L3 | L3 |
11 |
"RightStickClick" |
Right Stick (Click) | RSB | R3 | R3 |
12 |
"DpadUp" |
D-Pad Up | ⬆️ | ⬆️ | ⬆️ |
13 |
"DpadDown" |
D-Pad Down | ⬇️ | ⬇️ | ⬇️ |
14 |
"DpadLeft" |
D-Pad Left | ⬅️ | ⬅️ | ⬅️ |
15 |
"DpadRight" |
D-Pad Right | ➡️ | ➡️ | ➡️ |
Bindable helpers are available for the joysticks too:
Axis # | GamepadCode | Standard | Layout |
---|---|---|---|
0 |
"LeftStickLeft" "LeftStickRight"
|
Left Stick (X-Axis) | ⬅️➡️ |
1 |
"LeftStickUp" "LeftStickDown"
|
Left Stick (Y-Axis) | ⬆️⬇️ |
2 |
"RightStickLeft" "RightStickRight"
|
Right Stick (X-Axis) | ⬅️➡️ |
3 |
"RightStickUp" "RightStickDown"
|
Right Stick (Y-Axis) | ⬆️⬇️ |
[!TIP] Set the
joystick.pressThreshold
option inGamepadDevice.defaultOptions
to adjust event sensitivity.
gamepad.layout // "xbox_one"
Gamepad device layout reporting is a non-standard API, and should only be used for aesthetic enhancements improvements (i.e. display layout-specific icons).
Event | Description | Payload |
---|---|---|
"binddown" |
{name,button,buttonCode,device} |
A named bind button was pressed. |
Button presses: | ||
"Face1" |
{button,buttonCode,device} |
Standard layout button "Face1" was pressed. Equivalent to 0 . |
"Face2" |
{button,buttonCode,device} |
Standard layout button "Face2" was pressed. Equivalent to 1 . |
"Face3" |
{button,buttonCode,device} |
Standard layout button "Face3" was pressed. Equivalent to 2 . |
… | … | … |
Button presses (no label): | ||
0 or Button.A
|
{button,buttonCode,device} |
Button at offset 0 was pressed. |
1 or Button.B
|
{button,buttonCode,device} |
Button at offset 1 was pressed. |
2 or Button.X
|
{button,buttonCode,device} |
Button at offset 2 was pressed. |
… | … | … |
You can add custom devices to the device manager so it will be polled togehter and included in InputDevice.devices
.
import { type CustomDevice, InputDevice } from "pixijs-input-devices"
export const onScreenButtonsDevice: CustomDevice = {
type: "custom",
id: "OnScreen",
meta: {},
update: (now: number) => {
// polling update
}
};
InputDevice.add(onScreenButtonsDevice);
Use named binds to create mappings between abstract inputs and the keys/buttons that trigger those inputs.
This allows you to change the keys/buttons later (e.g. allow users to override inputs).
// keyboard:
InputDevice.keyboard.configureBinds({
jump: [ "ArrowUp", "Space", "KeyW" ],
crouch: [ "ArrowDown", "KeyS" ],
toggleGraphics: [ "KeyB" ],
})
// all gamepads:
GamepadDevice.configureDefaultBinds({
jump: [ "Face1", "LeftStickUp" ],
crouch: [ "Face2", "Face3", "RightTrigger" ],
toggleGraphics: [ "RightStickUp", "RightStickDown" ],
})
These can then be used with either the real-time and event-based APIs.
// listen to all devices:
InputDevice.onBindDown("toggleGraphics", (e) => toggleGraphics())
// listen to specific devices:
InputDevice.keyboard.onBindDown("jump", (e) => doJump())
InputDevice.gamepads[0].onBindDown("jump", (e) => doJump())
let jump = false, crouch = false, moveX = 0
const keyboard = InputDevice.keyboard
if (keyboard.bindDown("jump")) jump = true
if (keyboard.bindDown("crouch")) crouch = true
if (keyboard.key.ArrowLeft) moveX = -1
else if (keyboard.key.ArrowRight) moveX = 1
for (const gamepad of InputDevice.gamepads) {
if (gamepad.bindDown("jump")) jump = true
if (gamepad.bindDown("crouch")) crouch = true
// gamepads have additional analog inputs
// we're going to apply these only if touched
if (gamepad.leftJoystick.x != 0) moveX = gamepad.leftJoystick.x
if (gamepad.leftTrigger > 0) moveX *= (1 - gamepad.leftTrigger)
}
Traverse a UI using input devices.
Set up navigation once using:
UINavigation.configureWithRoot(app.stage) // any root container
registerPixiJSNavigationMixin(PIXI.Container)
Navigation should now work automatically if your buttons handle these events:
-
"pointerdown"
– i.e. Trigger / show press effect
But in order to really make use, you should also set:
-
"pointerover"
– i.e. Select / show hover effect -
"pointerout"
– i.e. Deselect / reset
[!TIP] 🖱️ Seamless navigation: Manually set
UINavigation.focusTarget = <target>
inside any"pointerover"
handlers to allow mouse/pointers to update the navigation context for all devices.
[!TIP] Auto-focus: Set a container's
navigationPriority
to a value above0
to become the default selection in a context.
The Navigation API is centered around the UINavigation manager, which receives navigation intents from devices and forwards it to the UI context.
The UINavigation manager maintains a stack of responders, which can be a
Container
, or any object that implements the NavigationResponder
interface.
When a device sends a navigation intent, the UINavigation manager is responsible for asking the first responder whether it can handle the intent.
If it returns false
, any other responders are checked (if they exist),
otherwise the default global navigation behavior kicks in.
When a navigation intent is not handled manually by a responder, it is handled in one of the following ways:
Intent | Behavior |
---|---|
"navigate.back" |
|
"navigate.left" , "navigate.right" , "navigate.up" , "navigate.down"
|
|
"navigate.trigger" |
|
Container event | Description | Compatibility |
---|---|---|
"devicedown" |
Target was triggered. |
"pointerdown" , "mousedown"
|
"deviceover" |
Target became focused. |
"pointerover" , "mouseover"
|
"deviceout" |
Target lost focus. |
"pointerout" , "mouseout"
|
Containers are extended with a few properties/accessors:
Container properties | type | default | description |
---|---|---|---|
isNavigatable |
get(): boolean |
false |
returns true if navigationMode is set to "target" , |
navigationMode |
"auto" | "disabled" | "target"
|
"auto" |
When set to "auto" , a Container can be navigated to if it has a "pointerdown" or "mousedown" event handler registered. |
navigationPriority |
number |
0 |
The priority relative to other navigation items in this group. |
[!NOTE] isNavigatable: By default, any element with
"pointerdown"
or"mousedown"
handlers is navigatable.
[!WARNING] Fallback Hover Effect: If there is no
"pointerover"
or"mouseover"
handler detected on a container,UINavigation
will apply abasic alpha effect to the selected item to indicate which container is currently the navigation target. This can be disabled by settingUINavigation.options.enableFallbackOverEffect
tofalse
.
The keyboard and gamepad devices are preconfigured with the following binds, feel free to modify them:
Navigation Intent Bind | Keyboard | Gamepad |
---|---|---|
"navigate.left" |
"ArrowLeft", "KeyA" | "DpadLeft", "LeftStickLeft" |
"navigate.right" |
"ArrowRight", "KeyD" | "DpadRight", "LeftStickRight" |
"navigate.up" |
"ArrowUp", "KeyW" | "DpadUp", "LeftStickUp" |
"navigate.down" |
"ArrowDown", "KeyS" | "DpadDown", "LeftStickDown" |
"navigate.trigger" |
"Enter", "Space" | "Face1" |
"navigate.back" |
"Escape", "Backspace" | "Face2", "Back" |
You can manually take control of navigation using:
// take control
UINavigation.pushResponder(myModalView)
// relinquish control
UINavigation.popResponder()
Use the <device>.meta
property to set assorted meta data on devices as needed.
You lose TypeScript's nice strong types, but its very handy for things like user assignment in multiplayer games.
InputDevice.on("deviceconnected", ({ device }) =>
// assign!
device.meta.localPlayerId = 123
)
for (const device of InputDevice.devices)
{
if (device.meta.localPlayerId === 123)
{
// use assigned input device!
}
}
You can easily map an on-screen input device using the CustomDevice
interface.
export class OnScreenInputContainer extends Container implements CustomDevice {
id = "onscreen"
type = "custom" as const
meta: Record<string, any> = {}
inputs = {
moveX: 0.0
jump: false,
}
update(now)
{
this.inputs.moveX = this._virtualJoystick.x
this.inputs.jump = this._jumpButton.isTouching()
}
// e.g. disable named binds for onscreen joysticks:
bindDown(name){ return false }
}
const onscreen = new OnScreenInputContainer()
InputDevice.add(onscreen)
InputDevice.remove(onscreen)
You could set up multiple named inputs:
InputDevice.keyboard.configureBinds({
jump: [ "ArrowUp", "KeyW" ],
defend: [ "ArrowDown", "KeyS" ],
left: [ "ArrowLeft", "KeyA" ],
right: [ "ArrowRight", "KeyD" ],
p1_jump: [ "KeyW" ],
p1_defend: [ "KeyS" ],
p1_left: [ "KeyA" ],
p1_right: [ "KeyD" ],
p2_jump: [ "ArrowUp" ],
p2_defend: [ "ArrowDown" ],
p2_left: [ "ArrowLeft" ],
p2_right: [ "ArrowRight" ]
})
and then switch groups depending on the mode:
if (gameMode === "multiplayer")
{
player1.jump = device.bindDown("p1_jump")
player1.defend = device.bindDown("p1_defend")
player1.moveX += device.bindDown("p1_left") ? -1 : 0
player1.moveX += device.bindDown("p1_right") ? 1 : 0
player2.jump = device.bindDown("p2_jump")
player2.defend = device.bindDown("p2_defend")
player2.moveX += device.bindDown("p2_left") ? -1 : 0
player2.moveX += device.bindDown("p2_right") ? 1 : 0
}
else
{
player1.jump = device.bindDown("jump")
player1.defend = device.bindDown("defend")
player1.moveX += device.bindDown("left") ? -1 : 0
player1.moveX += device.bindDown("right") ? 1 : 0
updateComputerPlayerInput(player2)
}