The site is under maintenance, so logging in is temporarily unavailable. All resources and the MCP still work.

AI Skills

Cross-platform input

By Roblox Vibecoders · roblox-input

Use when adding keybinds, controls or on-screen buttons to a Roblox game, detecting whether the player is on keyboard, gamepad or touch, or showing "press E" style prompts. Covers the Input Action System (InputContext, InputAction, InputBinding, Input Action Manager), UserInputService events and PreferredInput, ContextActionService touch buttons, mobile-only buttons, and key/button glyphs.

Install it

Terminal
mkdir -p ~/.claude/skills/roblox-input && curl -fsSL https://robloxvibecoders.com/skills/roblox-input/SKILL.md -o ~/.claude/skills/roblox-input/SKILL.md

Skills in ~/.claude/skills work in every project. Put them in your project's .claude/skills folder to share them with your team.

What your AI reads

SKILL.md
---
name: roblox-input
description: Use when adding keybinds, controls or on-screen buttons to a Roblox game, detecting whether the player is on keyboard, gamepad or touch, or showing "press E" style prompts. Covers the Input Action System (InputContext, InputAction, InputBinding, Input Action Manager), UserInputService events and PreferredInput, ContextActionService touch buttons, mobile-only buttons, and key/button glyphs.
---

# Cross-platform input

Most Roblox sessions are on phones, many are on consoles. Every action needs a keyboard/mouse binding, a gamepad
binding and a touch control, and every on-screen hint must match the device the player is using right now.

## Pick the API

| Need | Use |
|---|---|
| Gameplay actions (sprint, shoot, interact, ability 1) | **Input Action System**: `InputContext` > `InputAction` > `InputBinding` |
| Device detection, raw input (mouse position, scroll, any key) | **UserInputService** |
| Existing code built on `BindAction` | **ContextActionService** (Roblox plans to deprecate its binding methods; don't start new systems on it) |

Input exists only on the client. Read it in LocalScripts or client modules, then ask the server through a remote. The
server validates; it never trusts "the player pressed X" as proof of anything.

## Input Action System

Instances describe the controls; scripts only react. Build them in Studio with the **Input Action Manager**
(Window > Input > Input Action Manager), which creates them in an `Inputs` folder in ReplicatedStorage, flags actions
missing a binding for a device, and warns about duplicate keys in one context. Plain Explorer instances work too.

```text
ReplicatedStorage
  Inputs (Folder)
    PlayContext (InputContext)        -- gameplay controls
      Sprint (InputAction, Type = Bool)
        Keyboard (InputBinding, KeyCode = LeftShift)
        Gamepad (InputBinding, KeyCode = ButtonL3)
        Touch (InputBinding, UIButton = the on-screen SprintButton)
    MenuContext (InputContext, Enabled = false)
```

**InputContext**: `Enabled` turns all its actions on or off. `Priority`: higher runs first. `Sink`: when true, keys
bound in this context don't reach lower-priority contexts (same-priority contexts still get them). Swap contexts
when menus open: enable `MenuContext`, disable `PlayContext`, and back.

**InputAction.Type** (`Enum.InputActionType`):

| Type | State | Typical bindings |
|---|---|---|
| `Bool` (default) | `true`/`false` | keys, buttons, mouse buttons, a `UIButton`; triggers use `PressedThreshold`/`ReleasedThreshold` |
| `Direction1D` | number, -1 to 1 | triggers (`ButtonR2`), or `Up`/`Down` keys |
| `Direction2D` | `Vector2` | `Thumbstick1`, `MouseDelta`, `TouchDelta`, or `Up`/`Down`/`Left`/`Right` keys (WASD) |
| `Direction3D` | `Vector3` | the four directions plus `Forward`/`Backward` |
| `ViewportPosition` | `Vector2` pixel position | pointer position, for cursors and world picking |

**InputBinding** properties: `KeyCode`; composite `Up`/`Down`/`Left`/`Right`/`Forward`/`Backward`;
`PrimaryModifier`/`SecondaryModifier` (key combos); `UIButton` (a GuiButton drives a Bool action) and `UIModifier`;
`Scale`, `Vector2Scale`, `Vector3Scale`, `ResponseCurve`, `ClampMagnitudeToOne`; `DisplayName` and `DisplayImage`
for hints. `MouseDelta` and `TouchDelta` report pixels, so give those bindings a small `Scale` such as `0.01`.

```luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local playContext = ReplicatedStorage:WaitForChild("Inputs"):WaitForChild("PlayContext") :: InputContext
local sprint = playContext:WaitForChild("Sprint") :: InputAction
local steer = playContext:WaitForChild("Steer") :: InputAction -- Direction2D

sprint.Pressed:Connect(function() setSprinting(true) end)   -- Bool only, false -> true
sprint.Released:Connect(function() setSprinting(false) end) -- Bool only, true -> false
sprint.StateChanged:Connect(function(state) end)            -- any type, on every real change

RunService.PreRender:Connect(function()
	local direction = steer:GetState() :: Vector2 -- poll continuous actions each frame
	moveVehicle(direction)
end)
```

- To drive an action from code (your own joystick, a tutorial), set a binding's `Type` to
  `Enum.InputBindingType.Scriptable` and call `binding:Fire(state)`. `InputAction:Fire()` is deprecated.
- To show Roblox's own Move and Jump actions in the manager, set `Workspace.PlayerScriptsUseInputActionSystem` to
  `Enabled` in the Properties window. Give your gameplay context a high `Priority` (the docs use 2000) with `Sink` on
  if it must override default controls on the same keys.

## UserInputService

```luau
local UserInputService = game:GetService("UserInputService")

UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
	if gameProcessedEvent then
		return -- the engine already used it: typing in chat or a TextBox, clicking UI
	end
	if input.KeyCode == Enum.KeyCode.F then
		toggleFlashlight()
	elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
		fire(input.Position)
	end
end)
```

- `InputBegan`, `InputChanged` (mouse move, wheel, thumbsticks), `InputEnded`. The `InputObject` has `KeyCode`,
  `UserInputType`, `UserInputState`, `Position` and `Delta`.
- `IsKeyDown(keyCode)`, `GetMouseLocation()`, `GetMouseDelta()` (only while the mouse is locked), `MouseBehavior`,
  `MouseIconEnabled`, `GetConnectedGamepads()`, `GamepadConnected`/`GamepadDisconnected`.
- Touch gestures: `TouchTap`, `TouchSwipe`, `TouchPinch`, `TouchPan`, `TouchLongPress`, `TouchRotate`.

### Which device is the player using

Use `UserInputService.PreferredInput` (`Enum.PreferredInput`: `KeyboardAndMouse`, `Gamepad`, `Touch`,
`MicroGamepad`). It picks the primary device from what is connected and what was used last, without flickering.

```luau
local function applyInputType()
	local preferred = UserInputService.PreferredInput
	local touch = preferred == Enum.PreferredInput.Touch
	mobileButtons.Visible = touch -- on-screen buttons only for touch players
	hints.Visible = not touch      -- key hints only when there is a key to press
end

applyInputType()
UserInputService:GetPropertyChangedSignal("PreferredInput"):Connect(applyInputType)
```

- `TouchEnabled` is true on touchscreen laptops, so it is not an "is mobile" check.
- `GetLastInputType()` and `LastInputTypeChanged` thrash between mouse movement and keyboard, and return values like
  `TextInput` or `Focus`. Use them only when you need the exact last input type.

## Mobile-only buttons

With the Input Action System: make a GuiButton, point an InputBinding's `UIButton` at it, and show it only when
`PreferredInput` is `Touch` (code above). Place buttons in the bottom-right thumb area without covering the default
jump button, and make them big enough to hit with a thumb.

With ContextActionService (existing code):

```luau
local ContextActionService = game:GetService("ContextActionService")

local function onReload(actionName: string, state: Enum.UserInputState, input: InputObject)
	if state == Enum.UserInputState.Begin then
		reload()
	end
	return Enum.ContextActionResult.Sink -- Pass lets lower bindings see the input
end

-- When the gun is equipped; true = also create a touch button on touch devices
ContextActionService:BindAction("Reload", onReload, true, Enum.KeyCode.R, Enum.KeyCode.ButtonX)
ContextActionService:SetTitle("Reload", "Reload")
ContextActionService:SetPosition("Reload", UDim2.new(1, -160, 1, -160))
-- SetImage("Reload", imageId) and SetDescription also customize the button

-- When the gun is unequipped
ContextActionService:UnbindAction("Reload")
```

`GetButton(actionName)` returns the created ImageButton, or `nil` when no touch button exists (non-touch devices).
`BindActionAtPriority` adds a priority; otherwise the most recent binding on an input wins.

## Showing the right key or button

Never write "Press E" as plain text. The key differs per device and per keyboard layout (on AZERTY,
`Enum.KeyCode.Q` is the key labelled A).

- **No code:** an `InputActionLabel` (a GuiObject, currently in beta) with its `InputAction` set shows the right
  glyph or text and updates when the device changes.
- **Custom UI:** read `InputAction.PreferredBinding` (the binding that matches the current device) and listen for its
  change. Use the binding's `DisplayName`/`DisplayImage` if set, otherwise the platform's glyph.
- **Any key code:** `UserInputService:GetStringForKeyCode(keyCode)` returns the label for the player's layout or
  connected gamepad (pass `Enum.KeyCodeStringFormat.Abbreviated` for short names like "LCtrl").
  `UserInputService:GetImageForKeyCode(keyCode)` returns the Xbox or PlayStation button image for the connected
  controller, or an empty string when there is none.

```luau
local function updateHint(action: InputAction, imageLabel: ImageLabel, textLabel: TextLabel)
	local binding = action.PreferredBinding
	local keyCode = if binding then binding.KeyCode else Enum.KeyCode.None
	local image = if keyCode ~= Enum.KeyCode.None then UserInputService:GetImageForKeyCode(keyCode) else ""
	imageLabel.Image = image
	imageLabel.Visible = image ~= ""
	textLabel.Text = if binding and binding.DisplayName ~= "" then binding.DisplayName
		elseif keyCode ~= Enum.KeyCode.None then UserInputService:GetStringForKeyCode(keyCode)
		else ""
	textLabel.Visible = image == "" and textLabel.Text ~= ""
end

updateHint(sprint, sprintGlyph, sprintKey)
sprint:GetPropertyChangedSignal("PreferredBinding"):Connect(function()
	updateHint(sprint, sprintGlyph, sprintKey)
end)
```

On touch, hide key hints and show the on-screen button instead.

## Mistakes to avoid

- Ignoring `gameProcessedEvent`, so abilities fire while the player types in chat.
- Using `TouchEnabled` or `GetLastInputType()` to choose a UI layout. Use `PreferredInput`.
- An action with no gamepad or touch binding. Console and phone players simply can't do it.
- Hard-coded key names in text. Use the glyph APIs and refresh on device change.
- Binding the same key in ContextActionService and an InputContext. Pick one system per action.
- Leaving bindings active outside their context: unbind or disable when the tool is unequipped or the menu closes.
- Handling input on the server, or trusting input-driven claims without server checks.