Skip to content
ENSign in

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

RemoteEventRemoteFunction
The client learns why it was refusedonly with a second eventyes, in the return
Blocks the client while it waitsnoyes, until the server answers
A slow servernothing stallsthe button waits; disable it meanwhile
Use it forfire-and-forget: a claim, a spina 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

Ultimate Script Pack

Ultimate Script Pack

$40See the pack

Get notified

Coming soon

Drop your email and we'll ping you the moment this pack goes live.