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.
Use the robloxvibecoders MCP: call get_resource with id "skill-roblox-input" and install it as a Claude Code skill in this project (.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.