Tutorial · Scripting foundations

How to detect key presses with UserInputService

Almost every game needs a key that does something: E to interact, F to use an ability, Q to drop, M for the map. In Roblox you listen for keys in a LocalScript with UserInputService, because only the player's own device knows what they pressed. This guide starts with one keybind, then fixes the bug every first keybind has (it fires while you type in chat), adds held keys and mouse buttons, gives phone players a button, and sends the press to the server in a way a cheater cannot abuse.

Updated 5 November 20267 min readBeginnerLuauWritten by

In short

In a LocalScript in StarterPlayerScripts, connect UserInputService.InputBegan. The function gets an InputObject and a gameProcessedEvent boolean. Return early when that boolean is true (the player is typing in chat or clicking a button), then compare input.KeyCode with a key such as Enum.KeyCode.F. Use InputEnded for when the key is let go and UserInputService:IsKeyDown to ask if it is held right now.

  • UserInputService only works in LocalScripts. A server Script never sees key presses.
  • Always check gameProcessedEvent, or pressing F while chatting fires your ability.
  • Keys do not exist on phones. For touch, ContextActionService:BindAction with true adds an on-screen button for the same action.

Get the script

Keybind script with UserInputService

A LocalScript that runs a function when the player presses a key, ignores typing in chat and works for several keys from one table.

LocalScript in StarterPlayerScripts named Keybinds
-- LocalScript in StarterPlayerScripts named Keybinds.
-- Runs a function when the player presses a key, but not while they type in chat.
local UserInputService = game:GetService("UserInputService")

local function useAbility()
	print("Ability used")
end

local function dropItem()
	print("Item dropped")
end

-- One line per key. Add more keys here.
local ACTIONS: { [Enum.KeyCode]: () -> () } = {
	[Enum.KeyCode.F] = useAbility,
	[Enum.KeyCode.Q] = dropItem,
}

UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
	if gameProcessedEvent then
		return -- typing in chat or a TextBox, or pressing a button
	end
	local action = ACTIONS[input.KeyCode]
	if action then
		action()
	end
end)

RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts

Where to put it

Explorer: one LocalScript
StarterPlayer
  StarterPlayerScripts
    Keybinds LocalScript new

Settings you can change

Enum.KeyCode.FIn the script
The key for the first action. Add a line to ACTIONS for each key you want.

This only reacts on the player's device. To change anything other players see, send the press to the server as in Step 4.

Type-checked with luau-lsp strict against Roblox's API definitions on 5 November 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.

Prompt for RoCode

Add keybinds to my game: one LocalScript in StarterPlayerScripts using UserInputService that ignores key presses while the player types in chat, runs a function for F and another for Q, and sends the F press to the server through a RemoteEvent that checks a cooldown.

On this page
  1. Get the script
  2. How it works
  3. 1. First keybind
  4. 2. Hold and click
  5. 3. Phone and gamepad
  6. 4. Tell the server
  7. Common mistakes
  8. Common errors
  9. With RoCode
  10. Questions
  11. Sources

How key presses reach your script

Every key, mouse button, touch and gamepad button becomes an InputObject on the player's device. UserInputService fires events with it: InputBegan when the input starts, InputChanged while it moves (the mouse, a thumbstick) and InputEnded when it stops. Your function reads what kind of input it was and reacts.

The parts of an InputObject you use most
PropertyWhat it tells youExample
KeyCodeWhich key or gamepad button.Enum.KeyCode.E, Enum.KeyCode.ButtonX
UserInputTypeWhat kind of input: keyboard, a mouse button, touch, a gamepad.Enum.UserInputType.MouseButton1 (left click)
UserInputStateBegin, Change or End.Mostly useful with ContextActionService
PositionWhere on screen the mouse or finger is.For aiming or dragging

Because this all happens on the client, the script must be a LocalScript in a place where LocalScripts run: StarterPlayerScripts (runs once per session), StarterCharacterScripts (runs again for every new character) or StarterGui. The client and server guide lists every container.

Step 1: Your first keybind

  1. In the Explorer, open StarterPlayer, hover over StarterPlayerScripts, click + and insert a LocalScript. Name it Keybinds.
  2. Paste the code below and press Play. Press F and Q and watch the Output window.
  3. Click the chat box, type a message with an f in it, and check that nothing prints while you type.
LocalScript in StarterPlayerScripts named Keybinds
-- LocalScript in StarterPlayerScripts named Keybinds.
-- Runs a function when the player presses a key, but not while they type in chat.
local UserInputService = game:GetService("UserInputService")

local function useAbility()
	print("Ability used")
end

local function dropItem()
	print("Item dropped")
end

-- One line per key. Add more keys here.
local ACTIONS: { [Enum.KeyCode]: () -> () } = {
	[Enum.KeyCode.F] = useAbility,
	[Enum.KeyCode.Q] = dropItem,
}

UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
	if gameProcessedEvent then
		return -- typing in chat or a TextBox, or pressing a button
	end
	local action = ACTIONS[input.KeyCode]
	if action then
		action()
	end
end)

gameProcessedEvent is true when Roblox already used the input for something else: typing in the chat box or any TextBox, clicking a GUI button, or a core control such as the camera keys. Returning early in that case is what stops F from firing while someone types "fun". Keeping the keys in one table means adding a keybind is one new line.

Step 2: Held keys and mouse buttons

For something that lasts while a key is held, such as charging a throw or aiming, start it in InputBegan and stop it in InputEnded. Mouse buttons arrive through the same events, with UserInputType instead of KeyCode.

LocalScript in StarterPlayerScripts named ChargeThrow
-- LocalScript in StarterPlayerScripts named ChargeThrow.
-- Hold the right mouse button to charge, release to throw. Press R to check a held key.
local UserInputService = game:GetService("UserInputService")

local MAX_CHARGE_SECONDS = 2

local chargeStarted: number? = nil

UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
	if gameProcessedEvent then
		return
	end
	if input.UserInputType == Enum.UserInputType.MouseButton2 then
		chargeStarted = os.clock()
	elseif input.KeyCode == Enum.KeyCode.R then
		-- IsKeyDown asks about another key at this moment
		local running = UserInputService:IsKeyDown(Enum.KeyCode.LeftShift)
		print(if running then "Reload while sprinting" else "Reload")
	end
end)

UserInputService.InputEnded:Connect(function(input: InputObject)
	if input.UserInputType == Enum.UserInputType.MouseButton2 and chargeStarted then
		local held = math.min(os.clock() - chargeStarted, MAX_CHARGE_SECONDS)
		chargeStarted = nil
		print(string.format("Thrown with %.0f%% power", held / MAX_CHARGE_SECONDS * 100))
	end
end)

InputEnded does not check gameProcessedEvent here on purpose: if a player starts charging and then their mouse ends up over a button, the release should still end the charge. MouseButton1 is the left button and MouseButton2 the right one. For clicks on a GUI button, use the button's own Activated event instead, as the GUI button guide does.

Step 3: Phones and controllers

A phone has no F key. ContextActionService:BindAction binds one function to several inputs at once, and its third argument, true, creates an on-screen button for touch devices. This is the easiest way to make one ability work on keyboard, controller and phone.

LocalScript in StarterPlayerScripts named AbilityAction
-- LocalScript in StarterPlayerScripts named AbilityAction.
-- One action for the F key, the X button on a gamepad and an on-screen button on phones.
local ContextActionService = game:GetService("ContextActionService")

local ACTION_NAME = "UseAbility"

local function onAbility(_actionName: string, inputState: Enum.UserInputState, _input: InputObject): Enum.ContextActionResult
	if inputState == Enum.UserInputState.Begin then
		print("Ability used")
	end
	return Enum.ContextActionResult.Sink -- this press is handled; nothing else gets it
end

-- true = create a touch button on phones and tablets
ContextActionService:BindAction(ACTION_NAME, onAbility, true, Enum.KeyCode.F, Enum.KeyCode.ButtonX)
ContextActionService:SetTitle(ACTION_NAME, "Use")
Which one to use
You wantUse
To react to any key or click, or read the mouseUserInputService events
One action on keyboard, gamepad and touch, with a ready-made touch buttonContextActionService:BindAction
Controls laid out in Studio with your own touch button designThe Input Action System (InputAction and InputBinding)

Unbind an action with ContextActionService:UnbindAction(ACTION_NAME) when it should stop working, for example while a menu is open. The sprint guide shows the newer Input Action System with a sprint key.

Step 4: Tell the server about the key press

Anything other players should see, such as damage, a door opening or an item appearing, has to happen on the server. The LocalScript sends a RemoteEvent when the key is pressed, and the server decides whether the action is allowed. Insert a RemoteEvent named UseAbility in ReplicatedStorage. Then, in the Keybinds LocalScript from Step 1, make useAbility fire it. The whole client side then looks like this:

LocalScript in StarterPlayerScripts named Keybinds
-- LocalScript in StarterPlayerScripts named Keybinds (Step 4 version).
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")

local useAbilityRemote = ReplicatedStorage:WaitForChild("UseAbility") :: RemoteEvent

local function useAbility()
	useAbilityRemote:FireServer() -- the server decides what happens
end

local ACTIONS: { [Enum.KeyCode]: () -> () } = {
	[Enum.KeyCode.F] = useAbility,
}

UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
	if gameProcessedEvent then
		return
	end
	local action = ACTIONS[input.KeyCode]
	if action then
		action()
	end
end)

And a Script on the server receives it. The cooldown is kept on the server, because a cheater can fire the remote as fast as they like no matter what the LocalScript does.

Script in ServerScriptService named AbilityServer
-- Script in ServerScriptService named AbilityServer.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local COOLDOWN_SECONDS = 3

local useAbilityRemote = ReplicatedStorage:WaitForChild("UseAbility") :: RemoteEvent
local lastUse: { [Player]: number } = {}

useAbilityRemote.OnServerEvent:Connect(function(player: Player)
	local now = os.clock()
	if now - (lastUse[player] or 0) < COOLDOWN_SECONDS then
		return -- too soon: ignore
	end
	local character = player.Character
	local humanoid = character and character:FindFirstChildOfClass("Humanoid")
	if not humanoid or humanoid.Health <= 0 then
		return
	end
	lastUse[player] = now
	print(player.Name, "used their ability")
end)

Players.PlayerRemoving:Connect(function(player: Player)
	lastUse[player] = nil
end)

Send only the fact that the key was pressed, or the minimum the server needs, such as where the player aimed. Never send results like "deal 50 damage": the server works those out. The RemoteEvents guide covers checking what a remote receives.

Common mistakes in keybind scripts

What goes wrong, and the fix
MistakeWhat happensFix
UserInputService in a server ScriptNothing. The server never receives key events.Use a LocalScript in StarterPlayerScripts.
No gameProcessedEvent checkThe ability fires while the player types in chat.Return early when it is true.
input.KeyCode == "F"Never true: KeyCode is an Enum, not text.Compare with Enum.KeyCode.F.
Using the old Mouse.KeyDownWorks poorly and is deprecated.Use UserInputService.InputBegan.
Doing the effect in the LocalScript onlyOnly that player sees it; the server ignores it.Fire a RemoteEvent and do it on the server.
Cooldown only in the LocalScriptA cheater skips it.Keep the cooldown on the server too.

Common errors and fixes

Open the Output window (Window menu, then Output) before you press Play.

Pressing the key does nothing

Check it is a LocalScript (not a Script) and that it is in StarterPlayerScripts, StarterCharacterScripts or StarterGui, where LocalScripts run. A LocalScript in Workspace or ServerScriptService never runs. Then add print(input.KeyCode) at the top of the function to see what arrives.

It works once, then stops after I die

A LocalScript in StarterPlayerScripts keeps running after a respawn, but any character or Humanoid it stored at the start is the old one. Look the character up when the key is pressed, or move the script to StarterCharacterScripts.

The phone button does not appear

The third argument of BindAction must be true, and the button only shows on touch devices. In Studio, test with the Device emulator set to a phone.

The server never gets the key press

Check that the RemoteEvent is in ReplicatedStorage with the same name in both scripts, and that the server Script is in ServerScriptService. Infinite yield possible in Output means the name does not match.

Build it with RoCode

If you would rather not wire this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the scripts and instances in your Explorer, so there is no code to paste.

You type

Add an ability on the F key: a LocalScript that ignores typing in chat, works on gamepad and phone with ContextActionService, and fires a RemoteEvent; the server checks a 3 second cooldown and that the player is alive before doing anything.

RoCode does
  • Can look through your place first with Search Game and Search Scripts, so it adds to keybinds you already have instead of a second set
  • Writes the LocalScript and the server Script with Create Script and adds the RemoteEvent with Create Instance
  • Is told to compile-check each script it writes with Check Script, which compiles it in Studio without running it
  • Sets an undo point before and after each batch of changes, so Ctrl+Z steps back one batch at a time

With plugin 3.9.15 or newer RoCode can control a test character that walks and presses keys, but it checks only what it chose to test, so press your keys yourself too. How RoCode connects to Studio.

Questions

How do I detect a key press in Roblox?

In a LocalScript, connect UserInputService.InputBegan, return if gameProcessedEvent is true, and compare input.KeyCode with a key such as Enum.KeyCode.E. Step 1 has the full script.

Can a server Script use UserInputService?

No. Input only exists on the player's device. Detect the key in a LocalScript and send a RemoteEvent to the server.

How do I check if a key is being held down?

Call UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) at the moment you need to know, or track it yourself between InputBegan and InputEnded.

What is gameProcessedEvent?

The second value InputBegan gives you. It is true when Roblox already used the input, for example for typing in chat or clicking a GUI button. Ignore those inputs in your keybinds.

Should I use UserInputService or ContextActionService?

UserInputService to listen to any input; ContextActionService when one action should work on keyboard, controller and touch, because it can add a touch button for you.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 5 November 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.