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 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
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
Step 1: Build the button and the panel
- In the Explorer, hover over StarterGui, click + and insert a ScreenGui. Name it
MenuGui. - Into MenuGui, insert a TextButton named
ShopButton. Set itsTexttoShop,Sizeto{0.12, 0}, {0.07, 0}andPositionto{0.02, 0}, {0.45, 0}so it sits on the left side. - Into MenuGui, insert a Frame named
ShopFrame. SetAnchorPointto0.5, 0.5,Positionto{0.5, 0}, {0.5, 0}andSizeto{0.5, 0}, {0.6, 0}so it is centred. Untick Visible. - Into ShopFrame, insert a TextButton named
CloseButtonwithTextX,AnchorPoint1, 0,Position{1, -8}, {0, 8}andSize{0, 40}, {0, 40}, so it sits in the top right corner. - 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 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.
| Event | Fires for | Use it for |
|---|---|---|
Activated | Mouse click, tap, gamepad select | Almost every button |
MouseButton1Click | Mouse clicks and taps | Older code; prefer Activated |
MouseEnter / MouseLeave | The mouse moving over and off | Hover 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
TextScaledon buttons and add a UITextSizeConstraint with aMaxTextSize, 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(defaultCoreUISafeInsets) 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 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.
-- 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.
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.
- 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
- Roblox Creator Docs: Buttons. TextButton, ImageButton and Activated
- Roblox Creator Docs: GuiButton. Activated, MouseButton1Click and AutoButtonColor
- Roblox Creator Docs: Position and size UI objects. Scale, Offset and AnchorPoint
- Roblox Creator Docs: ScreenGui. ResetOnSpawn and ScreenInsets
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.