npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

awesome-react-gamepads

v1.0.1

Published

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

Readme

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