Need Game Art? Edit Game Trailers Launch Your Dev Site Sell Your Merch
Need Game Art? Sell Your Merch

How to Add Remappable Controls to Web Games

Updated July 2026
Remappable controls are the single most impactful motor accessibility feature in game development. Building them into a web game requires an action mapping abstraction layer that separates game logic from physical input, a rebinding system for both keyboard and gamepad inputs, conflict detection, persistent storage, and an accessible settings UI. This guide walks through the complete implementation.

Players who use adaptive controllers, one-handed setups, or non-standard input devices need the ability to assign game actions to whatever buttons they can physically reach. Players without disabilities also benefit from remappable controls because personal ergonomic preferences vary widely. Building remappable controls from the start costs a fraction of the effort required to retrofit them, because it requires a clean separation between input and game logic that also improves code quality.

Step 1: Create an Action Map Abstraction Layer

The foundation of remappable controls is an abstraction layer that sits between raw input events and game logic. Instead of the game loop checking "is the Space key pressed?" to trigger a jump, it checks "is the jump action active?" The mapping from "jump" to "Space key" lives in a data structure that can be edited at runtime.

Define an action map as a JavaScript object or Map. Each entry maps an action name (a string like "jump," "attack," "interact," "pause," "moveUp," "moveDown," "moveLeft," "moveRight") to a binding object containing the input type (keyboard, gamepad button, gamepad axis) and the input identifier (a key code like "KeyW," a gamepad button index like 0, or an axis index with a direction like { axis: 1, direction: -1 } for left-stick-up).

Each action also has metadata: whether it is a press action (single activation on key down), a hold action (active while held), or a toggle action (activated on press, deactivated on next press). This metadata drives the input state machine. The game logic reads the action's current state (pressed this frame, held, released this frame) without knowing which physical input triggered it.

Every action in the game, including menu navigation actions like "menuUp," "menuDown," "menuConfirm," and "menuBack," should go through this abstraction. If any game action bypasses the action map and checks raw input directly, that action cannot be remapped, which defeats the purpose. Discipline here is non-negotiable: never read raw input outside the action map system.

Step 2: Build the Keyboard Rebinding System

For keyboard input, use KeyboardEvent.code rather than KeyboardEvent.key. The code property identifies the physical key position on the keyboard (e.g., "KeyW" is the physical W key regardless of keyboard layout), while the key property identifies the character produced (which changes with keyboard layout, language, and modifier keys). A French player on an AZERTY keyboard has "Z" where "W" is on QWERTY, but the code "KeyW" refers to the same physical key on both. Using code ensures that bindings work consistently across keyboard layouts.

Store the current binding for each action. When the player enters the rebinding UI and selects an action to rebind, the system enters a "listen" state that captures the next keydown event. The captured event's code value becomes the new binding for that action. Display the new binding to the player using a human-readable label: convert "KeyW" to "W," "Space" to "Space," "ShiftLeft" to "Left Shift," etc. A lookup table mapping code values to display names handles this.

Support modifier keys as standalone bindings (a player may want "Left Shift" as their jump key) and as modifiers in combination bindings (Shift+W for a secondary action). Combination bindings are important for players with limited buttons who need more actions than they have keys; a shift-modified layer effectively doubles the available bindings from a set of reachable keys.

Prevent binding to keys that should not be overridden: browser function keys (F5 for refresh, F11 for fullscreen, F12 for dev tools) and system keys (Tab for focus navigation, Escape for browser UI). Show these as "reserved" in the rebinding UI. Also prevent binding to keys that your game framework uses internally, like the canvas element's tabindex-related keys.

Step 3: Add Gamepad API Button Remapping

The Gamepad API provides access to connected controllers through navigator.getGamepads(). Poll this each frame in the game loop (the API does not fire events for button presses; it provides a snapshot of the current state). Each Gamepad object has a buttons array (digital and analog buttons) and an axes array (analog sticks, typically axes[0] and axes[1] for the left stick, axes[2] and axes[3] for the right stick).

Map gamepad button indices to action names the same way keyboard codes are mapped. A default binding might assign button index 0 (typically the A/Cross button) to "jump" and button index 2 (X/Square) to "attack." The rebinding UI shows the current button assignment and lets the player press a gamepad button to reassign, the same flow as keyboard rebinding but detecting a gamepad button press instead of a keydown event.

For analog stick axes, allow binding axes to movement actions. Left stick axes[0] (horizontal) and axes[1] (vertical) typically map to movement. Right stick axes[2] and axes[3] map to camera control. Apply the player's dead zone setting (see the motor accessibility guide) before processing axis values. An axis value within the dead zone range is treated as zero.

Handle gamepad connection and disconnection events with the gamepadconnected and gamepaddisconnected events on the window object. When a gamepad connects, detect its type (standard mapping or vendor-specific) and apply appropriate default bindings. When a gamepad disconnects during gameplay, pause the game or display a notification rather than silently failing with no input.

The Xbox Adaptive Controller and similar adaptive devices present as standard gamepads to the browser via the Gamepad API. If your game supports full gamepad remapping, it automatically works with adaptive controllers, which is one of the key accessibility advantages of web games over native games that may require specific SDK support for adaptive hardware.

Step 4: Implement Conflict Detection and Resolution

When a player assigns a binding that is already used by another action, the system must handle the conflict. Display both conflicting actions with their bindings highlighted, and offer resolution options: unbind the other action (leaving it with no binding until the player assigns a new one), swap the bindings between the two actions, or allow the duplicate if the two actions never occur simultaneously (e.g., "jump" and "confirm menu selection" can share a key if the game state prevents both from being active at the same time).

Context-aware conflict detection is more sophisticated. Define action contexts, like "gameplay," "menu," "dialogue," and "inventory," and only flag conflicts within the same context. A key bound to "jump" in the gameplay context and "confirm" in the menu context is not a real conflict because the game never processes both contexts simultaneously. This allows more flexible rebinding with fewer false-positive conflict warnings.

Always provide a "Reset to Defaults" button that restores the original binding map. Players who experiment with rebinding and end up in a confusing state need a quick escape hatch. This button should also be accessible from the main menu, not just from within the rebinding UI, in case the player remaps essential navigation keys and cannot reach the rebinding screen.

Step 5: Save and Restore Bindings with localStorage

Serialize the action map to a JSON string and store it in localStorage under a key like "gameBindings." On game load, before the first frame, check localStorage for saved bindings and apply them. If no saved bindings exist, use the defaults. Include a version number in the saved data so that when you add new actions in an update, the game can detect an outdated binding map, merge the new defaults with the player's saved customizations, and save the merged result.

Store keyboard and gamepad bindings separately, so a player can have custom keyboard bindings and default gamepad bindings (or vice versa). This is important because a player may customize one input method extensively while never touching the other.

For games with user accounts, consider also saving bindings server-side so the player's configuration persists across devices. The localStorage save handles the common case; server-side backup handles the edge case of switching computers or clearing browser data.

Step 6: Build an Accessible Rebinding UI

The rebinding settings screen must itself be accessible. It should be fully navigable with keyboard alone (arrow keys to select actions, Enter to start rebinding, Escape to cancel). Show a clear list of all actions grouped by context (movement, combat, menus, etc.) with the current binding displayed next to each action name. When the player selects an action for rebinding, show a clear prompt ("Press a key or button...") and a countdown timer or cancel option.

Display bindings using appropriate labels: key names for keyboard ("W," "Space," "Left Shift"), button icons or names for gamepad ("A Button," "Left Trigger," "Right Stick Up"). If the game detects a specific controller model (Xbox, PlayStation, generic), show the corresponding button icon. The Gamepad API's mapping property ("standard" or "") indicates whether the browser has mapped the controller to the standard layout.

Include preset layouts for common configurations: Default, Left-Handed, One-Handed Right, One-Handed Left, and ESDF (an alternative to WASD that gives more surrounding keys for actions). These presets give players a starting point to customize rather than requiring them to rebuild from scratch. Each preset is simply a predefined action map that the player can further modify.

Key Takeaway

Remappable controls require an action map abstraction that all game logic queries instead of raw inputs, a rebinding system for both keyboard (using KeyboardEvent.code) and gamepad (using button indices), conflict detection with context awareness, and persistent storage in localStorage. Build this system before writing game logic, not after.