Tutorial · Menus, HUD and sound

How to make a GUI button that opens a menu in Roblox

Shops, inventories, settings and codes menus all start the same way: a button on screen that opens a panel, and a way to close it again. This guide builds that from nothing in Studio, with a LocalScript that toggles the panel, a close button, a rule that only one panel is open at a time, a keyboard shortcut, and sizes that still fit on a phone. It ends with a button that asks the server to do something, the safe way.

Updated 5 November 20266 min readBeginnerLuauWritten by

In short

In StarterGui, add a ScreenGui with a TextButton and a Frame (untick the Frame's Visible). Put a LocalScript inside the ScreenGui that connects the button's Activated event and sets frame.Visible = not frame.Visible. Use Activated rather than MouseButton1Click, because it fires for mouse clicks, taps and gamepad presses alike.

  • GUI code runs in LocalScripts. A server Script cannot see a player's clicks.
  • Size with Scale (fractions of the screen), not only Offset (pixels), so the UI fits phones.
  • A button that changes the game (buys, teleports, gives items) only asks the server; the server decides.

Get the script

GUI button script to open and close a frame

A LocalScript that opens and closes menu frames from their buttons, closes the others, adds a close button and a keyboard shortcut.

LocalScript in the MenuGui ScreenGui named MenuButtons
-- LocalScript inside the MenuGui ScreenGui, named MenuButtons.
-- Each button opens its frame; opening one closes the others.
local UserInputService = game:GetService("UserInputService")

local gui = script.Parent :: ScreenGui

type Menu = { button: string, frame: string, key: Enum.KeyCode? }

-- One line per menu: the button's name, the frame's name and an optional key.
local MENUS: { Menu } = {
	{ button = "ShopButton", frame = "ShopFrame", key = Enum.KeyCode.M },
}

local frames: { Frame } = {}

local function closeAll()
	for _, frame in frames do
		frame.Visible = false
	end
end

local function toggle(frame: Frame)
	local opening = not frame.Visible
	closeAll()
	frame.Visible = opening
end

for _, menu in MENUS do
	local button = gui:WaitForChild(menu.button) :: GuiButton
	local frame = gui:WaitForChild(menu.frame) :: Frame
	table.insert(frames, frame)

	button.Activated:Connect(function()
		toggle(frame)
	end)

	local close = frame:FindFirstChild("CloseButton")
	if close and close:IsA("GuiButton") then
		close.Activated:Connect(function()
			frame.Visible = false
		end)
	end

	local key = menu.key
	if key then
		UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
			if not gameProcessedEvent and input.KeyCode == key then
				toggle(frame)
			end
		end)
	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: the ScreenGui, a button, its frame and the LocalScript
StarterGui
  MenuGui ScreenGui new
    ShopButton TextButton new
    ShopFrame Frame new
      CloseButton TextButton new
    MenuButtons LocalScript new

Settings you can change

MENUSIn the script
Pairs of button and frame names. Add a line for each menu, such as { button = "BagButton", frame = "BagFrame", key = Enum.KeyCode.B }.

Untick Visible on each frame so menus start closed.

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 a menu bar: a ScreenGui with Shop and Bag buttons on the left that open and close their frames, each frame with a close X button, only one open at a time, M and B as keyboard shortcuts, sized with Scale so it fits phones.

On this page
  1. Get the script
  2. 1. Build the UI
  3. 2. Toggle script
  4. 3. Phone sizing
  5. 4. Server action
  6. Common errors
  7. With RoCode
  8. Questions
  9. Sources

Step 1: Build the button and the panel

  1. In the Explorer, hover over StarterGui, click + and insert a ScreenGui. Name it MenuGui.
  2. Into MenuGui, insert a TextButton named ShopButton. Set its Text to Shop, Size to {0.12, 0}, {0.07, 0} and Position to {0.02, 0}, {0.45, 0} so it sits on the left side.
  3. Into MenuGui, insert a Frame named ShopFrame. Set AnchorPoint to 0.5, 0.5, Position to {0.5, 0}, {0.5, 0} and Size to {0.5, 0}, {0.6, 0} so it is centred. Untick Visible.
  4. Into ShopFrame, insert a TextButton named CloseButton with Text X, AnchorPoint 1, 0, Position {1, -8}, {0, 8} and Size {0, 40}, {0, 40}, so it sits in the top right corner.
  5. Optional: add a UICorner to the button and the frame for rounded corners.

The numbers in curly brackets are UDim2 values: {scale, offset} for X and then Y. Scale is a fraction of the parent (0.5 is half the screen); offset is in pixels. Using mostly scale makes the panel the same share of the screen on a laptop and a phone.

Step 2: Open and close it with a LocalScript

Insert a LocalScript into MenuGui named MenuButtons. It handles any number of menus from one table, closes the others when one opens, and gives each menu a keyboard shortcut.

LocalScript in the MenuGui ScreenGui named MenuButtons
-- LocalScript inside the MenuGui ScreenGui, named MenuButtons.
-- Each button opens its frame; opening one closes the others.
local UserInputService = game:GetService("UserInputService")

local gui = script.Parent :: ScreenGui

type Menu = { button: string, frame: string, key: Enum.KeyCode? }

-- One line per menu: the button's name, the frame's name and an optional key.
local MENUS: { Menu } = {
	{ button = "ShopButton", frame = "ShopFrame", key = Enum.KeyCode.M },
}

local frames: { Frame } = {}

local function closeAll()
	for _, frame in frames do
		frame.Visible = false
	end
end

local function toggle(frame: Frame)
	local opening = not frame.Visible
	closeAll()
	frame.Visible = opening
end

for _, menu in MENUS do
	local button = gui:WaitForChild(menu.button) :: GuiButton
	local frame = gui:WaitForChild(menu.frame) :: Frame
	table.insert(frames, frame)

	button.Activated:Connect(function()
		toggle(frame)
	end)

	local close = frame:FindFirstChild("CloseButton")
	if close and close:IsA("GuiButton") then
		close.Activated:Connect(function()
			frame.Visible = false
		end)
	end

	local key = menu.key
	if key then
		UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
			if not gameProcessedEvent and input.KeyCode == key then
				toggle(frame)
			end
		end)
	end
end

Press Play: Shop opens the panel, X or Shop again closes it, and M does the same. To add a Bag menu, build a BagButton and a BagFrame the same way and add one line to MENUS. The gameProcessedEvent check stops M toggling the shop while the player types in chat; the UserInputService guide explains it.

Which button event to use
EventFires forUse it for
ActivatedMouse click, tap, gamepad selectAlmost every button
MouseButton1ClickMouse clicks and tapsOlder code; prefer Activated
MouseEnter / MouseLeaveThe mouse moving over and offHover effects

Step 3: Make it fit phones

  • Keep shapes square. A button sized only with scale is wider on a wide screen. Add a UIAspectRatioConstraint to the close button (AspectRatio 1) and it stays square everywhere.
  • Keep text readable. Tick TextScaled on buttons and add a UITextSizeConstraint with a MaxTextSize, so the text fits small buttons but does not become huge on a monitor.
  • Mind the top bar and notches. Roblox's buttons sit in the top left, and phones have notches. ScreenGui's ScreenInsets (default CoreUISafeInsets) keeps your UI out of those areas; leave it on.
  • Test on devices. In Studio's Test tab, use the Device emulator to view the UI as a phone and a tablet.

For buttons that grow a little when hovered and panels that slide in instead of appearing, the UI animations guide adds tweens to these same buttons.

Step 4: A button that asks the server

Opening a panel only changes the player's own screen, so the LocalScript does it alone. A button that changes the game, such as "Claim reward" or "Respawn", has to ask the server. Insert a RemoteEvent named ClaimReward in ReplicatedStorage. Inside ShopFrame, add a TextButton named ClaimButton with a LocalScript:

LocalScript in the ClaimButton named ClaimButton
-- LocalScript inside the ClaimButton TextButton.
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local claimReward = ReplicatedStorage:WaitForChild("ClaimReward") :: RemoteEvent
local button = script.Parent :: TextButton

button.Activated:Connect(function()
	claimReward:FireServer()
	button.Text = "Claimed!"
	button.Active = false
	button.AutoButtonColor = false
end)
Script in ServerScriptService named ClaimRewardServer
-- Script in ServerScriptService named ClaimRewardServer.
-- Gives 50 coins once per player per server.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local claimReward = ReplicatedStorage:WaitForChild("ClaimReward") :: RemoteEvent
local claimed: { [Player]: boolean } = {}

claimReward.OnServerEvent:Connect(function(player: Player)
	if claimed[player] then
		return -- the button can be fired again by a cheater; the server says no
	end
	claimed[player] = true
	local leaderstats = player:FindFirstChild("leaderstats")
	local coins = leaderstats and leaderstats:FindFirstChild("Coins")
	if coins and coins:IsA("IntValue") then
		coins.Value += 50
	end
end)

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

Greying the button out on the client is for honest players. The claimed table on the server is what actually stops a second claim, because a cheater can fire the RemoteEvent without the button. For a reward that should be claimable once ever, save it: the daily rewards guide does that.

Common errors and fixes

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

Clicking the button does nothing

Check that the script is a LocalScript inside the ScreenGui (or another place LocalScripts run), and that the names in MENUS match the Explorer exactly. Infinite yield possible in Output means a name does not match. Also check nothing transparent lies on top of the button: a Frame above it catches the click.

The frame does not appear

It may be open but off screen, or behind something. Set its ZIndex higher, check its Position and Size, and that the ScreenGui itself is Enabled.

The menu closes when I respawn

ScreenGuis with ResetOnSpawn ticked are rebuilt on every respawn. Untick it on MenuGui if menus should stay as they were.

Players.LocalPlayer.PlayerGui is not a valid member

A server Script is reaching into GUI. Move the code to a LocalScript; see X is not a valid member of Y.

Build it with RoCode

If you would rather not build the UI 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 GUI and scripts in your Explorer, so there is no code to paste.

You type

Add a menu bar on the left with Shop, Bag and Settings buttons. Each opens its own centred frame with a close X, only one open at a time, keyboard shortcuts M, B and P, sized with Scale and aspect ratio constraints so it fits phones.

RoCode does
  • Can check your existing StarterGui first with Search Game, so it matches your current menus instead of adding a second set
  • Creates the ScreenGui, buttons and frames with Create Instance and writes the LocalScript with Create Script
  • Is told to compile-check each script it writes with Check Script
  • Sets an undo point before and after each batch of changes, so Ctrl+Z steps back one batch at a time

RoCode also has ready-made game screens, such as a shop and an inventory, in its UI templates. Check the result in Studio's Device emulator at phone size. How RoCode connects to Studio.

Questions

How do I make a button open a GUI in Roblox Studio?

Put a LocalScript in the ScreenGui that connects the button's Activated event and sets the frame's Visible to not frame.Visible. Step 2 has a version for several menus.

Should I use Activated or MouseButton1Click?

Activated. It fires for mouse clicks, taps and gamepad presses, so the button works on every device.

Why is my GUI a different size on mobile?

It is sized in pixels (Offset). Use Scale for sizes and positions, plus UIAspectRatioConstraint to keep shapes, and check in the Device emulator.

Can a server Script open a player's GUI?

It should not. Send a RemoteEvent to the player and let a LocalScript open the GUI, or set an attribute the LocalScript watches.

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.