Skip to content

ChristopherHButler/awesome-react-gamepads

Repository files navigation

awesome-react-gamepads

React hooks for the browser Gamepad API — buttons, axes, rumble, sequences, and multiplayer out of the box.

npm version install size license

awesome-react-gamepads is a lightweight React hook library that wraps the native browser Gamepad API. It handles the polling loop, dead zones, button hold detection, haptics, controller profiles, and custom DOM events so you can focus on building your game or UI.

Live Demo

Live demo →

Connect a gamepad and explore the controller visualizer, docs, and playable games built with the library.

Features

  • useGamepads — track all connected gamepads with a full callback and event API
  • useGamepad(index) — single-controller variant; use multiple instances for local multiplayer
  • useGamepadSequence — detect arbitrary button combos or cheat codes (Konami, fighting-game specials, etc.)
  • Context API — GamepadsProvider, useGamepadsContext, and withGamepads HOC; one polling loop, any depth
  • Controller profiles — xbox, playstation, switch, generic; buttonLabels maps button names for your UI
  • Haptics / rumble via rumble() with duration, weakMagnitude, strongMagnitude, startDelay
  • Button hold / long-press detection via onGamepadButtonHold
  • Dead zone presets ("none" | "small" | "medium" | "large") or a raw number
  • Configurable poll rate — requestAnimationFrame (default) or a fixed setInterval interval
  • Konami code built-in via onKonamiSuccess
  • SSR / Next.js safe — all window and navigator calls are guarded
  • Ships as ESM, CommonJS, and UMD bundles with full TypeScript types

Installation

npm install awesome-react-gamepads

Peer dependencies: React 16.8 or later.

Quick Start

import { useGamepads } from 'awesome-react-gamepads';

function Game() {
  const { gamepad, rumble } = useGamepads({
    onA: () => {
      jump();
      rumble({ duration: 80, strongMagnitude: 0.6 });
    },
  });

  return <p>{gamepad?.connected ? 'Controller connected' : 'No controller'}</p>;
}

API

useGamepads(options?)

Tracks all connected gamepads. Polls via requestAnimationFrame by default.

import { useGamepads } from 'awesome-react-gamepads';

const { gamepad, rumble, profile, buttonLabels } = useGamepads(options);

Props (UseGamepadsProps)

All props are optional.

Prop Type Default Description
deadZone number | "none" | "small" | "medium" | "large" "medium" Axis values below this threshold are clamped to 0. Presets: none=0, small=0.05, medium=0.08, large=0.15.
stickThreshold number 0.75 Value above which directional stick callbacks (onLeftStickRight, etc.) fire.
holdThreshold number (ms) 500 Duration a button must be held before onGamepadButtonHold fires.
pollRate number (ms) When set, uses setInterval at this interval instead of requestAnimationFrame.
controllerProfile ControllerProfile "xbox" Active button-naming profile. Affects ButtonDetails.buttonName and buttonLabels.
onConnect (gamepad: ReactGamepad) => void Fired when a gamepad connects.
onDisconnect (gamepad: ReactGamepad) => void Fired when a gamepad disconnects.
onUpdate (gamepad: ReactGamepad) => void Fired on every poll cycle where state changed.
onGamepadButtonDown (button: ButtonDetails) => void Fired on any button press.
onGamepadButtonUp (button: ButtonDetails) => void Fired on any button release.
onGamepadButtonChange (button: ButtonDetails) => void Fired on any button state change (down or up).
onGamepadButtonHold (button: ButtonDetails) => void Fired once when a button has been held longer than holdThreshold.
onA (button: ButtonDetails) => void Bottom face button (index 0) pressed.
onB (button: ButtonDetails) => void Right face button (index 1) pressed.
onX (button: ButtonDetails) => void Left face button (index 2) pressed.
onY (button: ButtonDetails) => void Top face button (index 3) pressed.
onLB (button: ButtonDetails) => void Left shoulder (index 4) pressed.
onRB (button: ButtonDetails) => void Right shoulder (index 5) pressed.
onLT (button: ButtonDetails) => void Left trigger (index 6) pressed.
onRT (button: ButtonDetails) => void Right trigger (index 7) pressed.
onSelect (button: ButtonDetails) => void Back / Select button (index 8) pressed.
onStart (button: ButtonDetails) => void Start / Menu button (index 9) pressed.
onLS (button: ButtonDetails) => void Left stick click (index 10) pressed.
onRS (button: ButtonDetails) => void Right stick click (index 11) pressed.
onDPadUp (button: ButtonDetails) => void D-Pad Up (index 12) pressed.
onDPadDown (button: ButtonDetails) => void D-Pad Down (index 13) pressed.
onDPadLeft (button: ButtonDetails) => void D-Pad Left (index 14) pressed.
onDPadRight (button: ButtonDetails) => void D-Pad Right (index 15) pressed.
onXBoxLogo (button: ButtonDetails) => void Home / Guide button (index 16) pressed.
onGamepadAxesChange (axes: AxesDetails) => void Fired when any axis value changes.
onLeftStickRight (axes: AxesDetails) => void Left stick crosses stickThreshold rightward.
onLeftStickLeft (axes: AxesDetails) => void Left stick crosses stickThreshold leftward.
onLeftStickUp (axes: AxesDetails) => void Left stick crosses stickThreshold upward.
onLeftStickDown (axes: AxesDetails) => void Left stick crosses stickThreshold downward.
onRightStickRight (axes: AxesDetails) => void Right stick crosses stickThreshold rightward.
onRightStickLeft (axes: AxesDetails) => void Right stick crosses stickThreshold leftward.
onRightStickUp (axes: AxesDetails) => void Right stick crosses stickThreshold upward.
onRightStickDown (axes: AxesDetails) => void Right stick crosses stickThreshold downward.
onKonamiSuccess () => void Fired when the Konami code (↑↑↓↓←→←→BA) is entered.

Per-button callbacks (onA, onB, etc.) always refer to the same physical button position regardless of the active profile — onA always fires for button index 0 (bottom face button). Use buttonLabels from the return value to display the profile-correct name in your UI.

Return value (UseGamepadsReturn)

Field Type Description
gamepad ReactGamepad | undefined Current state snapshot of the active gamepad. undefined before first connection.
rumble (options: RumbleOptions) => Promise<void> Trigger haptic feedback. No-ops silently if unsupported.
profile ControllerProfile The active controller profile ("xbox", "playstation", etc.).
buttonLabels Record<string, string> Maps Xbox button names to the active profile's display names.

useGamepad(index, options?)

Tracks a single gamepad by index. Accepts the same options as useGamepads and returns the same value. Useful for local multiplayer where each player needs an isolated hook.

import { useGamepad } from 'awesome-react-gamepads';

function Game() {
  const { gamepad: p1, rumble: rumble1 } = useGamepad(0, {
    onA: () => jump(1),
    controllerProfile: 'xbox',
  });

  const { gamepad: p2, rumble: rumble2 } = useGamepad(1, {
    onA: () => jump(2),
    controllerProfile: 'playstation',
  });

  return (
    <>
      <p>P1: {p1?.connected ? 'ready' : 'disconnected'}</p>
      <p>P2: {p2?.connected ? 'ready' : 'disconnected'}</p>
    </>
  );
}

useGamepadSequence(sequence, callback, options?)

Detects an arbitrary button sequence and fires callback when it is matched in order. Works standalone — no useGamepads call required in the same component.

Sequence items can be button names ("A", "Cross") or raw Standard Gamepad indices (0, 1, 2…).

import { useGamepadSequence } from 'awesome-react-gamepads';

// Konami code
useGamepadSequence(
  ['DPadUp','DPadUp','DPadDown','DPadDown','DPadLeft','DPadRight','DPadLeft','DPadRight','B','A'],
  () => activateCheats(),
);

// Fighting-game special with a 2-second input window between presses
useGamepadSequence(
  ['DPadDown', 'DPadRight', 'A'],
  () => fireHadouken(),
  { timeout: 2000 },
);

// PlayStation button names
useGamepadSequence(
  ['Cross', 'Circle', 'Cross'],
  () => doCombo(),
  { controllerProfile: 'playstation' },
);

// Raw indices
useGamepadSequence([0, 1, 0], () => doSomething());

Options (UseGamepadSequenceOptions)

Option Type Default Description
timeout number (ms) 0 Maximum time allowed between consecutive inputs before progress resets. 0 means no limit.
resetOnMiss boolean true Reset progress when a wrong button is pressed.
controllerProfile ControllerProfile "xbox" Profile used to resolve button names in the sequence.

Return value (UseGamepadSequenceReturn)

Field Type Description
reset () => void Manually reset sequence progress back to the beginning.

Context API

Mount a single GamepadsProvider at the top of your tree. All descendants can read the gamepad state with useGamepadsContext() without prop-drilling, and without starting extra polling loops.

GamepadsProvider accepts all the same props as useGamepads, including all callbacks.

import { GamepadsProvider, useGamepadsContext } from 'awesome-react-gamepads';

function App() {
  return (
    <GamepadsProvider controllerProfile="playstation" onA={() => jump()}>
      <Game />
    </GamepadsProvider>
  );
}

function HUD() {
  const { gamepad, buttonLabels, rumble } = useGamepadsContext();
  return (
    <div>
      <p>Press {buttonLabels.A} to fire</p>
      <button onClick={() => rumble({ duration: 200, strongMagnitude: 0.8 })}>
        Rumble
      </button>
    </div>
  );
}

useGamepadsContext() throws a descriptive error when called outside a <GamepadsProvider>.

withGamepads(Component)

HOC for class components (or any component that cannot call hooks directly). Requires a GamepadsProvider ancestor. The injected props match UseGamepadsReturn.

import { withGamepads, WithGamepadsProps, GamepadsProvider } from 'awesome-react-gamepads';

interface OwnProps {
  playerName: string;
}

class PlayerHUD extends React.Component<OwnProps & WithGamepadsProps> {
  render() {
    const { playerName, gamepad, buttonLabels } = this.props;
    return <p>{playerName}: press {buttonLabels.A} to jump</p>;
  }
}

export default withGamepads(PlayerHUD);

// In App (GamepadsProvider must be an ancestor):
// <GamepadsProvider><PlayerHUD playerName="P1" /></GamepadsProvider>

Controller Profiles

Pass controllerProfile to any hook or the GamepadsProvider to switch button naming conventions. The underlying physical layout (Standard Gamepad indices) is the same across all profiles — only the names change.

Profile Face buttons Shoulders Triggers Back / Start Home
xbox A, B, X, Y LB, RB LT, RT Select, Start Xbox
playstation Cross, Circle, Square, Triangle L1, R1 L2, R2 Share, Options PS
switch B, A, Y, X L, R ZL, ZR Minus, Plus Home
generic Button0–3 Button4–5 Button6–7 Button8–9 Button16

The buttonLabels field on the return value maps Xbox names to the active profile's names. Use it to render the correct label in your UI without hardcoding profile-specific strings:

const { buttonLabels } = useGamepads({ controllerProfile: 'playstation' });

<p>Press {buttonLabels.A} to confirm</p>   // → "Press Cross to confirm"
<p>Press {buttonLabels.LB} to sprint</p>   //  "Press L1 to sprint"

Per-button callbacks (onA, onB, onX, onY, etc.) are always named after the Xbox layout and map to the same physical button index on every profile. onA fires for button index 0 regardless of whether the connected controller calls it "A", "Cross", or "B".

Haptics / Rumble

The rumble function returned by any hook triggers haptic feedback via GamepadHapticActuator.playEffect('dual-rumble', …).

interface RumbleOptions {
  duration: number;        // milliseconds
  weakMagnitude?: number;  // 0–1, default 0.5  (high-frequency motor)
  strongMagnitude?: number;// 0–1, default 0.5  (low-frequency motor)
  startDelay?: number;     // milliseconds, default 0
}
const { rumble } = useGamepads();

// Sharp hit feedback
rumble({ duration: 100, strongMagnitude: 1.0, weakMagnitude: 0.3 });

// Gentle continuous vibration
rumble({ duration: 500, strongMagnitude: 0.2, weakMagnitude: 0.2 });

// Delayed secondary pulse
rumble({ duration: 150, strongMagnitude: 0.8, startDelay: 200 });

rumble is an async function that resolves when the effect completes. It catches and silently discards any error so it is always safe to call. If the browser or controller does not support haptics, it is a no-op.

Browser support: Chrome and Edge support dual-rumble. Firefox and Safari do not expose the haptics API — rumble silently does nothing on those browsers.

Dead Zones

The deadZone option clamps small axis values to zero, preventing stick drift from triggering callbacks.

Preset Value
"none" 0
"small" 0.05
"medium" 0.08 (default)
"large" 0.15

A raw number is also accepted for precise control:

useGamepads({ deadZone: 0.12 });

Custom DOM Events

Every hook also dispatches custom events on document so non-React code can react to gamepad input. All events bubble and include a detail object.

Event Fired when detail shape
gamepadupdated Each poll cycle where state changed { gamepad }
gamepadbuttondown Any button pressed { gamepad: number, buttonDetails: ButtonDetails }
gamepadbuttonup Any button released { gamepad: number, buttonDetails: ButtonDetails }
gamepadbuttonchange Any button state change { gamepad: number, buttonDetails: ButtonDetails }
axeschange Any axis value changes { gamepad: number, axes: AxesDetails }
leftStickXRight Left stick crosses threshold rightward { gamepad: number, axes: AxesDetails }
leftStickXLeft Left stick crosses threshold leftward { gamepad: number, axes: AxesDetails }
leftStickYUp Left stick crosses threshold upward { gamepad: number, axes: AxesDetails }
leftStickYDown Left stick crosses threshold downward { gamepad: number, axes: AxesDetails }
rightStickXRight Right stick crosses threshold rightward { gamepad: number, axes: AxesDetails }
rightStickXLeft Right stick crosses threshold leftward { gamepad: number, axes: AxesDetails }
rightStickYUp Right stick crosses threshold upward { gamepad: number, axes: AxesDetails }
rightStickYDown Right stick crosses threshold downward { gamepad: number, axes: AxesDetails }
useEffect(() => {
  const handler = (e: CustomEvent) => console.log('button pressed', e.detail.buttonDetails);
  document.addEventListener('gamepadbuttondown', handler as EventListener);
  return () => document.removeEventListener('gamepadbuttondown', handler as EventListener);
}, []);

TypeScript

All types are exported from the package root.

import type {
  UseGamepadsProps,
  UseGamepadsReturn,
  UseGamepadSequenceOptions,
  UseGamepadSequenceReturn,
  ButtonDetails,
  AxesDetails,
  ReactGamepad,
  RumbleOptions,
  ControllerProfile,
  WithGamepadsProps,
} from 'awesome-react-gamepads';

ButtonDetails

Passed to all button callbacks.

interface ButtonDetails {
  buttonIndex: number;   // Standard Gamepad button index (0–16)
  buttonName: string;    // Profile-specific name, e.g. "A", "Cross", "B"
  pressed: boolean;
  touched: boolean;
  value: string;         // Analog value as a string (useful for triggers)
}

AxesDetails

Passed to all axes callbacks.

interface AxesDetails {
  axesIndex: number;     // Standard Gamepad axes index
  axesName: string;      // One of: LeftStickX, LeftStickY, RightStickX, RightStickY, LeftTrigger, RightTrigger
  value: number;         // Current value after dead zone applied
  previousValue: number; // Value on the previous poll
}

Valid axesName values: LeftStickX, LeftStickY, RightStickX, RightStickY, LeftTrigger, RightTrigger.

ControllerProfile

type ControllerProfile = 'xbox' | 'playstation' | 'switch' | 'generic';

Browser Compatibility

Browser Support Notes
Chrome Full Gamepad API and haptics both supported
Edge Full Gamepad API and haptics both supported
Firefox Partial Gamepad API supported; haptics API not available — rumble is a no-op
Safari Partial Gamepad API support is limited; haptics not available — rumble is a no-op

rumble catches all errors internally and never throws, so it is safe to call on any browser without wrapping in a try/catch.

SSR / Next.js

All accesses to window, navigator, and document are guarded with typeof window !== 'undefined' checks. The hooks return immediately during server-side rendering without starting any polling loop and without throwing, making them safe in Next.js App Router and Pages Router server components or pages with SSR enabled.

License

MIT

About

A react hook, context and HOC to use the browser Gamepad API in react applications

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors