⚡ Powerful, high-performance input device management for PixiJS
🎮 Handles keyboards, gamepads, and more! | 🚀 Flexible update and event-driven APIs |
⚡ Optimized for INP performance | 🪄 Built-in named binds |
🔮 Highly configurable | 🌐 Built-in international keyboard support |
✅ Cross-platform & mobile-friendly [1] [2] [3] | 🧭 Built-in UI navigation (optional) |
🍃 Zero dependencies & tree-shakeable | ✨ Supports PixiJS v8, v7, v6.3+ |
Handle device inputs with ease.
import { InputDevice } from "pixijs-input-devices";
// Iterative
let jump = false
for (const device of InputDevice.devices) {
if (device.type === "keyboard" && device.key.Space) jump = true
if (device.type === "gamepad" && device.button.A) jump = true
}
// Event-driven
const gamepad = InputDevice.gamepads[0]
gamepad?.on("LeftShoulder", (e) => {
e.device.playVibration({ duration: 100 })
});
Everything you need to quickly integrate powerful device management.
PixiJS Input Devices adds first-class support for input device management and input handling. It also provides an optional navigation manager that can enable input devices to traverse pointer-based UIs.
The core concepts are:
- Devices: Any human interface device
- Binds: Custom, named input actions that can be triggered by assigned keys or buttons
- Navigation: A global controller that allows non-pointer devices to navigate UIs
Note
See Navigation 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(ticker => 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 Navigation API
import * as PIXI from 'pixi.js';
import { Navigation, registerPixiJSInputDevicesMixin } from 'pixijs-input-devices';
// register container mixin
registerPixiJSInputDevicesMixin(PIXI.Container);
const app = new PIXI.Application(/*…*/)
// set the root view for device navigation
Navigation.stage = app.stage
✨ You are now ready to use inputs!
The InputDevice
singleton controls all device discovery.
InputDevice.keyboard // KeyboardDevice
InputDevice.gamepads // Array<GamepadDevice>
InputDevice.custom // Array<CustomDevice>
You can access all active/connected devices using .devices
:
for ( const device of InputDevice.devices ) { // …
Property | Type | Description |
---|---|---|
InputDevice.isMobile |
boolean |
Whether the context is mobile (including tablets). |
InputDevice.isTouchCapable |
boolean |
Whether the context has touchscreen capability. |
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[] |
Custom devices. |
Access global events directly through the manager:
InputDevice.on( "deviceadded", ({ device }) => {
// a device was connected
// 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. |
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. See keyboard.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. |
"bind" |
{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":
let gamepad = InputDevice.gamepads[0]
if ( gamepad.button.Start ) { // …
if ( gamepad.leftTrigger > 0.25 ) { // …
if ( gamepad.leftJoystick.x > 0.5 ) { // …
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 # | ButtonCode | Standard | Nintendo* | Playstation | Xbox |
---|---|---|---|---|---|
0 |
"A" |
A | A | Cross | A |
1 |
"B" |
B | X | Circle | B |
2 |
"X" |
X | B | Square | X |
3 |
"Y" |
Y | Y | Triangle | Y |
4 |
"LeftShoulder" |
Left Shoulder | L | L1 | LB |
5 |
"RightShoulder" |
Right Shoulder | R | R1 | RB |
6 |
"LeftTrigger" |
Left Trigger | L2 | ZL | LT |
7 |
"RightTrigger" |
Right Trigger | R2 | ZR | RT |
8 |
"Back" |
Back | Minus | Options | Back |
9 |
"Start" |
Start | Plus | Select | Start |
10 |
"LeftStick" |
Left Stick (click) | L3 | L3 | LSB |
11 |
"RightStick" |
Right Stick (click) | R3 | R3 | RSB |
12 |
"DPadUp" |
D-Pad Up | ⬆️ | ⬆️ | ⬆️ |
13 |
"DPadDown" |
D-Pad Down | ⬇️ | ⬇️ | ⬇️ |
14 |
"DPadLeft" |
D-Pad Left | ⬅️ | ⬅️ | ⬅️ |
15 |
"DPadRight" |
D-Pad Right | ➡️ | ➡️ | ➡️ |
*See Nintendo Layout Remapping for more context
gamepad.layout // "nintendo" | "xbox" | "playstation" | "logitech" | "steam" | "generic"
Layout detection is highly non-standard across major browsers, it should generally be used for aesthetic improvements (e.g. showing device-specific icons).
There is some limited layout remapping support built-in for Nintendo controllers, which appear to be the only major brand controller that deviates from the standard.
Caution
*Nintendo: Both the labels and physical positions of the A,B,X,Y buttons are different on Nintendo controllers.
Set GamepadDevice.defaultOptions.remapNintendoMode
to apply the remapping as required.
"physical"
(default) – The A,B,X,Y button codes will refer the physical layout of a standard controller (Left=X, Top=Y, Bottom=A, Right=B)."accurate"
– The A,B,X,Y button codes will correspond to the exact Nintendo labels (Left=Y, Top=X, Bottom=B, Right=A)."none"
– The A,B,X,Y button codes mapping stay at the default indices (Left=Y, Top=B, Bottom=X, Right=A).
standard nintendo nintendo nintendo
layout "physical" "accurate" "none"
reference (default)
Y Y X B
X B X B Y A Y A
A A B X
3 3 2 1
2 1 2 1 3 0 3 0
0 0 1 2
You can manually override this per-gamepad, or for all gamepads:
gamepad.options.remapNintendoMode = "none"
GamepadDevice.defaultOptions.remapNintendoMode = "none"
Event | Description | Payload |
---|---|---|
"bind" |
{name,button,buttonCode,device} |
A named bind button was pressed. |
Button presses: | ||
"A" |
{button,buttonCode,device} |
Standard layout button "A" was pressed. Equivalent to 0 . |
"B" |
{button,buttonCode,device} |
Standard layout button "B" was pressed. Equivalent to 1 . |
"X" |
{button,buttonCode,device} |
Standard layout button "X" 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 myDevice: CustomDevice = {
id: "on-screen-buttons",
type: "custom",
meta: {},
update: ( now: number ) => {
// polling update
}
}
InputDevice.add( myDevice )
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.options.binds = {
jump: [ "ArrowUp", "Space", "KeyW" ],
crouch: [ "ArrowDown", "KeyS" ],
toggleGraphics: [ "KeyB" ],
}
// all gamepads:
GamepadDevice.defaultOptions.binds = {
jump: [ "A" ],
crouch: [ "B", "X", "RightTrigger" ],
toggleGraphics: [ "RightStick" ],
}
These can then be used with either the real-time and event-based APIs.
// listen to all devices:
InputDevice.onBind( "toggleGraphics", ( e ) => toggleGraphics() )
// listen to specific devices:
InputDevice.keyboard.onBind( "jump", ( e ) => doJump() )
InputDevice.gamepads[0].onBind( "jump", ( e ) => doJump() )
let jump = false, crouch = false, moveX = 0
const keyboard = InputDevice.keyboard
if ( keyboard.bindPressed( "jump" ) ) jump = true
if ( keyboard.bindPressed( "crouch" ) ) crouch = true
if ( keyboard.key.ArrowLeft ) moveX = -1
else if ( keyboard.key.ArrowRight ) moveX = 1
for ( const gamepad of InputDevice.gamepads ) {
if ( gamepad.bindPressed( "jump" ) ) jump = true
if ( gamepad.bindPressed( "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.
The Navigation API is centered around a central Navigation controller, which listens to navigation intents from devices, then handles the intent.
The Navigation controller maintains a stack of NavigationResponder
objects, which represent the current navigation context. For
example, you might add a NavigationResponder
for a drop-down UI. A normal Container
can be used as a NavigationResponder
, and any
container on the stack will become the current root container.
Note
The current root container is the top-most Container
on the navigation responder stack, or otherwise Navigation.stage
.
When a device sends a navigation intent, the Navigation controller is responsible for asking each of the responders on the stack if it can handle the intent. If it can't, it is propagated up all the way to the current root container.
When a navigation intent is not handled manually by a responder, it is handled in one of the following ways:
Intent | Behavior |
---|---|
"navigateBack" |
|
"navigateLeft" , "navigateRight" , "navigateUp" , "navigateDown" |
|
"trigger" |
|
Container event | Description | Equivalent
-----------------|--------------------------------------------------------
trigger
| Target was triggered. | "pointerdown"
, "mousedown"
focus
| Target became focused. | "pointerover"
, "mouseover"
blur
| Target lost focus. | "pointerout"
, "mouseout"
Containers are extended with a few properties/accesors:
Container properties | type | default | description |
---|---|---|---|
isNavigatable |
get(): boolean |
false |
returns true if navigationMode is set to "target" , or is "auto" and a "pointerdown" or "mousedown" event handler is registered. |
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, Navigation
will apply abasic alpha effect to the selected item to indicate which container is currently the navigation target. This
can be disabled by setting Navigation.options.useFallbackHoverEffect
to false
.
You can disable the navigation API entirely, either permanently or temporaril):
Navigation.options.enabled = false
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.moveX = this._virtualJoystick.x
this.jump = this._jumpButton.isTouching()
}
}
const onscreen = new OnScreenInputContainer();
InputDevice.add( onscreen )
InputDevice.remove( onscreen )
You could set up multiple named inputs:
InputDevice.keyboard.options.binds = {
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.bindPressed( "p1_jump" )
player1.defend = device.bindPressed( "p1_defend" )
player1.moveX += device.bindPressed( "p1_left" ) ? -1 : 0
player1.moveX += device.bindPressed( "p1_right" ) ? 1 : 0
player2.jump = device.bindPressed( "p2_jump" )
player2.defend = device.bindPressed( "p2_defend" )
player2.moveX += device.bindPressed( "p2_left" ) ? -1 : 0
player2.moveX += device.bindPressed( "p2_right" ) ? 1 : 0
}
else
{
player1.jump = device.bindPressed( "jump" )
player1.defend = device.bindPressed( "defend" )
player1.moveX += device.bindPressed( "left" ) ? -1 : 0
player1.moveX += device.bindPressed( "right" ) ? 1 : 0
updateComputerPlayerInput( player2 )
}