How do I make a shop GUI in Roblox?
Write the catalogue once, in a module both sides read. The client builds a card per item from a template; a RemoteFunction asks to buy and gets back either the new balance or a reason it was refused. The price is never read from the GUI, because the GUI is on the player's machine.
In short
- One catalogue module, read by the client and by the server.
- RemoteFunction, so the refusal can carry a reason the UI can show.
- Cards from a template and a UIGridLayout; never twenty hand-placed frames.
The shop is where a game's economy is either airtight or wide open, and the deciding question is small: who knows the price? If the server reads the price from the catalogue it owns, the GUI can say anything it likes and nothing happens.
Steps
1. One catalogue, in ReplicatedStorage
A ModuleScript both sides require. Two lists of prices is how an item ends up costing 100 on screen and 10 in the till.
-- ReplicatedStorage/ShopCatalogue (ModuleScript)
return {
{ id = "sword_wood", name = "Wooden Sword", price = 100, icon = "rbxassetid://0" },
{ id = "sword_iron", name = "Iron Sword", price = 450, icon = "rbxassetid://0" },
{ id = "boots_speed", name = "Speed Boots", price = 1200, icon = "rbxassetid://0" },
{ id = "pickaxe_gold", name = "Gold Pickaxe", price = 3000, icon = "rbxassetid://0" },
}
2. Build the grid from the table
One template frame, cloned per item. Adding an item is a line in the module, not a frame dragged into place.
-- StarterPlayerScripts (LocalScript)
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local catalogue = require(ReplicatedStorage:WaitForChild("ShopCatalogue"))
local buy = ReplicatedStorage:WaitForChild("BuyItem") -- a RemoteFunction
local gui = Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("Shop")
local grid = gui.Window:WaitForChild("Items") -- Frame with a UIGridLayout
local template = gui.Window:WaitForChild("CardTemplate")
local status = gui.Window:WaitForChild("Status") -- a TextLabel for refusals
for _, item in catalogue do
local card = template:Clone()
card.Name = item.id
card.ItemName.Text = item.name
card.Price.Text = string.format("%d", item.price)
card.Icon.Image = item.icon
card.Visible = true
card.Parent = grid
card.Buy.Activated:Connect(function()
card.Buy.Active = false
-- The id is all that travels. Never the price.
local result = buy:InvokeServer(item.id)
card.Buy.Active = true
status.Text = result.ok and "" or result.reason
end)
end
3. Refuse on the server, with a reason
A RemoteFunction because the UI has something to say either way. Validate the id against the catalogue the server requires itself.
-- ServerScriptService (Script)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local catalogue = require(ReplicatedStorage:WaitForChild("ShopCatalogue"))
local byId = {}
for _, item in catalogue do
byId[item.id] = item
end
local buy = Instance.new("RemoteFunction")
buy.Name = "BuyItem"
buy.Parent = ReplicatedStorage
local lastBuy: { [number]: number } = {}
buy.OnServerInvoke = function(player: Player, itemId: unknown)
-- Everything arriving here came from a client: check the type first.
local item = type(itemId) == "string" and byId[itemId] or nil
if not item then return { ok = false, reason = "That item does not exist." } end
local now = os.clock()
if lastBuy[player.UserId] and now - lastBuy[player.UserId] < 0.5 then
return { ok = false, reason = "Slow down." }
end
lastBuy[player.UserId] = now
local stats = player:FindFirstChild("leaderstats")
local coins = stats and stats:FindFirstChild("Coins")
if not coins then return { ok = false, reason = "Your data is still loading." } end
if coins.Value < item.price then
return { ok = false, reason = `You need {item.price - coins.Value} more coins.` }
end
coins.Value = coins.Value - item.price
-- grantItem is your own: give the tool, set the stat, write the save.
grantItem(player, item.id)
return { ok = true, balance = coins.Value }
end
4. Open and close the window
One button, one Visible. A shop that animates in on a tween is nicer, but a shop that can be closed is required.
local gui = Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("Shop")
gui.Window.Visible = false
gui.OpenButton.Activated:Connect(function()
gui.Window.Visible = not gui.Window.Visible
end)
gui.Window.CloseButton.Activated:Connect(function()
gui.Window.Visible = false
end)
RemoteEvent or RemoteFunction
| RemoteEvent | RemoteFunction | |
|---|---|---|
| The client learns why it was refused | only with a second event | yes, in the return |
| Blocks the client while it waits | no | yes, until the server answers |
| A slow server | nothing stalls | the button waits; disable it meanwhile |
| Use it for | fire-and-forget: a claim, a spin | a purchase, where the answer matters |
FAQ
Can the client just send the price?
Never. Anything a client sends is a value a player can choose, so the price has to be the server's own — read from the module the server requires, keyed by an id it validated. This is the single mistake that empties a game's economy.
Why is my grid all in one column?
A UIGridLayout sizes cells from CellSize and CellPadding, and a parent with no width gives it nothing to divide. Give the items frame a real size (or a UIAspectRatioConstraint) and check that the template is not Visible = false on the clone — the clone keeps whatever the template had.
How do I show "owned" instead of a price?
Send the player's owned ids with the shop open (one event), and swap the card's button text when the id is in that set. Deciding it from a client-side list is fine here: being wrong only shows the wrong label, and the server still refuses a second purchase.
Should the shop be in StarterGui or built in code?
Build the window in StarterGui so you can see it while you design, and build the cards in code from the catalogue. Hand-placing one frame per item is the version that goes stale the first time a price changes.
Updated: Tested in Studio: Roblox Studio, October 2026Author: GustavoLicense: Code in this guide may be used in any Roblox experience, including a paid one
Related pack
