UI Designer

On this page

Scripting your UI

Your exported UI already opens screens, plays sounds, saves settings and sells currency packs. This page shows how to hook it to your game: what its buttons fire, how currencies stay in sync, how to sell developer products and game passes safely, and how to save data.

The two hook scripts#

Every export comes with two scripts that connect the UI to your game: the .rbxmx, the starter place and Send to Studio all include them. They are written from your design, so they already list every button, event, product and setting it has:

ScriptWhereWhat it does
<Name>_UIHooks (LocalScript)StarterPlayer > StarterPlayerScriptsAn empty ui:on handler for every event the UI fires, grouped by screen, plus an Examples table for currencies, the HUD and screens.
<Name>_ServerHooks (Script)ServerScriptServiceChecked handlers for Remote actions, delivery hooks for products and rewards the UI can't pay by itself, and, on its last line, UI.Server.start({...}) with options picked for this UI.
  • From the .rbxmx: the server script ships turned off, so it can't run from Workspace. When the game runs, BloxUI_Setup moves both scripts into place and turns the server one on. If this UI's hook scripts are already installed, it keeps those, so a game never runs two copies.
  • Send to Studio installs both in the same undo step as the UI. Sending again updates them, except a script you've edited: that one is kept exactly as you left it.
  • Both carry the attributes BloxUIHooks (your UI's name), BloxUIHooksSide (Client or Server) and BloxUIHooksHash. That's how an install tells your edited scripts apart. Leave them on and keep the scripts' names.

On the design page: Make it work in your game#

Under your design on the UI Designer, the Make it work in your game box shows the same checklist in plain words: how to put the UI in your game, then what's left to do. Copy AI prompt copies a prompt for a coding AI with this UI's events, server actions, currencies and products, and both scripts once the design is exported (how to use it). How to hook it up opens this page.

If the scripts are missing#

A UI installed before exports included the scripts, or scripts you deleted: make them again from the design in the place. Open Studio's Command Bar (in the View tab in the classic layout; search Studio's menus for "Command Bar" if you don't see it), paste the whole block, change NAME to your UI's name and press Enter.

Command Bar: make the hook scripts
-- Only if <Name>_UIHooks / <Name>_ServerHooks are missing (exports include them). Makes both, tagged like an export's,
-- so Send to Studio updates them later. Paste it all into the Command Bar and press Enter.
local NAME = "NeonStrike" -- your UI's name: the StringValue in ReplicatedStorage > BloxUI_Blueprints

local RS = game:GetService("ReplicatedStorage")
local pack = workspace:FindFirstChild("BloxUI_" .. NAME) -- a dropped .rbxmx that hasn't run yet
local runtime = RS:FindFirstChild("BloxUI") or (pack and pack:FindFirstChild("BloxUI"))
local folder = RS:FindFirstChild("BloxUI_Blueprints") or (pack and pack:FindFirstChild("BloxUI_Blueprints"))
local value = folder and folder:FindFirstChild(NAME)
assert(runtime and value, "Can't find BloxUI and the UI " .. NAME .. " in this place")

local UI = require(runtime)
local Integration = UI.Blueprint.Integration
local out = Integration.generate((UI.Blueprint.decode(value.Value)), { name = NAME })
local places = {
	{ "LocalScript", "_UIHooks", "Client", game:GetService("StarterPlayer"):FindFirstChildOfClass("StarterPlayerScripts"), out.client },
	{ "Script", "_ServerHooks", "Server", game:GetService("ServerScriptService"), out.server },
}
for _, p in places do
	for _, child in p[4]:GetChildren() do
		local mine = child:GetAttribute(Integration.TAG) == NAME and child:GetAttribute(Integration.SIDE) == p[3]
		assert(not mine and child.Name ~= NAME .. p[2], NAME .. p[2] .. " is already in " .. p[4].Name)
	end
end
for _, p in places do
	local s = Instance.new(p[1])
	s.Name = NAME .. p[2]
	s.Source = p[5]
	s:SetAttribute(Integration.TAG, NAME)
	s:SetAttribute(Integration.SIDE, p[3])
	s:SetAttribute(Integration.HASH, Integration.hash(p[5]))
	s.Parent = p[4]
end
print("Added " .. NAME .. "_UIHooks and " .. NAME .. "_ServerHooks. Checklist items to do: " .. out.checklist.todo)

It uses the generator inside the BloxUI runtime (UI.Blueprint.Integration), the same one that writes the scripts for exports, and tags them the same way. It never replaces a script: if one is already there, it stops and says so.

What's inside#

  • A header with the checklist: product ids to add, rewards only your code can give, event-only settings.
  • Handlers that are empty but safe. A delivery hook returns false until your code gives something, so an unfinished stub never marks a purchase as paid.
  • Server handlers that check every value a client sends before using it.
  • An Examples table nothing calls: copy the lines you need.

Fill in the handlers your game needs and delete the rest. The smallest possible client script looks like this:

LocalScript: talk to the UI
-- A LocalScript in StarterPlayer > StarterPlayerScripts
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UI = require(ReplicatedStorage:WaitForChild("BloxUI"))

-- BloxUI_BlueprintLoader builds the UI and registers it under its name
local ui = UI.Blueprint.waitFor("NeonStrike", 30)
if ui == nil then
	warn("The UI NeonStrike wasn't built: is BloxUI_BlueprintLoader in StarterPlayerScripts?")
	return
end

ui:on("Rebirth", function(context)
	print("Rebirth pressed on", context.screen, "by button", context.id)
end)

ui:open("shop")

Events: ui:on#

ui:on(name, fn) runs fn(context) when the UI fires name. context.id is the element (a button id), context.screen its screen (nil on the HUD), context.payload what it sent. Handlers run on the client, so use them for effects and for asking the server.

UIHooks: ui:on
-- An Event action: { "kind": "Event", "name": "Rebirth" } on a button
ui:on("Rebirth", function(context)
	-- context.id     = the button's id ("doRebirth")
	-- context.screen = the screen it is on ("rebirth")
	-- context.payload = what the element sent (nil for a plain button)
end)

-- Every click also fires the element's own id, whatever its action
ui:on("doRebirth", function(context)
	print("the Rebirth button was pressed")
end)

-- An Event with a payload (a teleport row button: { "kind": "Event", "name": "Teleport", "payload": "shop" })
ui:on("Teleport", function(context)
	print("go to", context.payload) -- "shop"
end)

-- Screens opening and closing
ui:on("ScreenOpened", function(context)
	print("opened", context.screen)
end)
ui:on("ScreenClosed", function(context)
	print("closed", context.screen)
end)

-- ui:on returns a function that removes the handler again
local stop = ui:on("Equip", function(context)
	print("equip", context.payload) -- the item id
end)
stop()

Sections fire their own events. The hook scripts stub the ones your UI has:

EventFired whencontext.payload
Purchasedsomething was bought with an in-game currency (the server already took the price){ section, item }
Upgradean upgrade level was bought{ id, level }
Equip, Unequip, Favoriteinventory buttonsthe item id
DailyClaima daily reward day was pressed (display only)the day number
QuestClaima finished quest was claimed (display only)the quest id
PassClaima pass tier was claimed (display only){ tier, track }
CaseOpeneda case or egg reveal played (the server rolled it)the drop { name, icon, rarity }
SpinWon, RewardClaimeda wheel stopped, a reward was claimed (the server already gave it){ index, wedge }, { kind, item, data }
Teleporta teleport row button or a Worlds cardthe place id, e.g. "shop"
TradeAccept, TradeDeclinethe Trade window's buttonsnone
Loadedthe loading screen finishednone
ScreenOpened, ScreenClosedany screen opens or closesthe screen id

Currencies#

Each currency has a source that decides who owns the number:

SourceWhere the number livesUse it for
leaderstatplayer.leaderstats.<Name> (UI.Server creates it)the main currency, shown on the player list
attributethe player attribute <Name>second currencies (gems, tokens)
noneonly on the player's screendecoration; anything bought with it only happens on that screen

The UI Designer makes currencies that products, passes or cases pay into server-owned (leaderstat or attribute), so the server can deliver them. The counters follow the leaderstat or attribute by themselves: change the number on the server and the UI updates. You never set a counter from the client.

On the server#

ServerHooks: give and read currency
-- In <Name>_ServerHooks (server), above UI.Server.start
local Players = game:GetService("Players")

-- A part players touch to collect 25 coins (once every 10 seconds per player)
local coinPad = workspace:WaitForChild("CoinPad")
local lastTouch = {}

coinPad.Touched:Connect(function(hit)
	local player = Players:GetPlayerFromCharacter(hit.Parent)
	if player == nil then
		return
	end
	local now = os.clock()
	if lastTouch[player] and now - lastTouch[player] < 10 then
		return
	end
	lastTouch[player] = now
	UI.Server.grant(player, "coins", 25) -- saved, and waits while the player's data loads
end)

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

-- Reading a balance on the server
local function canAffordSword(player)
	return (UI.Server.getCurrency(player, "coins") or 0) >= 500
end

UI.Server.grant(player, id, amount) adds (or, with a negative amount, takes) and is saved. While a player's data is still loading, grants wait and are added once it's in. UI.Server.getCurrency and UI.Server.setCurrency read and set the number directly.

On the client#

UIHooks: read the counter, play the fly-in
-- In <Name>_UIHooks (client)
local coins = UI.peek(ui.currencies.coins) -- the number the counter shows right now

-- Run code whenever the counter changes (ui.currencies.<id> is a Fusion value)
local scope = UI.scope()
scope:Observer(ui.currencies.coins):onChange(function()
	print("Coins is now", UI.peek(ui.currencies.coins))
end)

-- The "+100" fly-in. For a leaderstat / attribute currency this only plays the effect:
-- the server changes the real balance and the counter follows it.
ui:give("coins", 100)                                    -- from the middle of the screen
ui:give("coins", 100, workspace.CoinChest)               -- from a part in the world

To play the fly-in when the server pays (a chest, a kill reward), let the server tell that player's screen:

ServerHooks
-- Server (ServerHooks): pay, then tell that player's screen to play the fly-in.
-- Make a RemoteEvent named "CoinsEarned" in ReplicatedStorage first.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CoinsEarned = ReplicatedStorage:WaitForChild("CoinsEarned")

local function reward(player, amount, fromPart)
	if UI.Server.grant(player, "coins", amount) then
		CoinsEarned:FireClient(player, amount, fromPart)
	end
end
UIHooks
-- Client (UIHooks): only the effect. The counter already follows the leaderstat.
local CoinsEarned = game:GetService("ReplicatedStorage"):WaitForChild("CoinsEarned")

CoinsEarned.OnClientEvent:Connect(function(amount, fromPart)
	ui:give("coins", amount, fromPart)
end)

Opening and closing screens#

Screen ids are listed in the Examples table of <Name>_UIHooks (for example shop, inventory, settings). HUD buttons already open their screens, and a screen with an openKey toggles on that key.

UIHooks: screens
-- In <Name>_UIHooks (client). Screen ids are in the Examples table at the bottom of the script.
ui:open("shop")
ui:close("shop")
ui:toggle("inventory")
if ui:isOpen("settings") then
	ui:close("settings")
end
ui:closeAll()

-- Open the shop when the player uses a ProximityPrompt on a stand in the world
local prompt = workspace:WaitForChild("ShopStand"):WaitForChild("ProximityPrompt")
prompt.Triggered:Connect(function()
	ui:open("shop")
end)

-- Open a screen when the server says so (a RemoteEvent "OpenScreen" in ReplicatedStorage)
local OpenScreen = game:GetService("ReplicatedStorage"):WaitForChild("OpenScreen")
OpenScreen.OnClientEvent:Connect(function(screenId)
	if typeof(screenId) == "string" then
		ui:open(screenId)
	end
end)

Opening a window closes the other open windows. The start screen (a main menu or loading screen) opens by itself when the UI is built.

Buttons and actions#

Every button in the design has an action. You can read them in the blueprint JSON (ReplicatedStorage.BloxUI_Blueprints.<Name>):

ActionWhat happensYour code
{"kind": "Open", "target": "shop"}
Close, Toggle
opens, closes or toggles a screen (Close without a target closes its own screen)none
{"kind": "Purchase", "productId": 123}Roblox's developer product promptsee products
{"kind": "GamePass", "gamePassId": 123}Roblox's game pass promptsee game passes
{"kind": "Remote", "name": "Rebirth"}asks the server: UI.Server.on("Rebirth", fn) runs therea server handler (ServerHooks stubs it)
{"kind": "Event", "name": "Rebirth"}fires ui:on("Rebirth") on the client; an optional payload (up to 64 characters) arrives as context.payloada client handler (UIHooks stubs it)
{"kind": "Notify", "text": "Coming soon!"}a small message on screennone
{"kind": "None"}only fires the button's own idui:on("<button id>")

Every click also fires the button's id, whatever its action, so ui:on("playButton", fn) always works.

Remote actions and server checks#

A Remote action sends (elementId, payload) to the server through the BloxUI_Action RemoteEvent that UI.Server creates. It is rate-limited (20 per second per player) and shape-checked (a string id of up to 100 characters, plain values, tables at most 2 levels deep). Deciding whether the player may do it is your job:

ServerHooks: a checked Remote handler
-- The button's action in the design: { "kind": "Remote", "name": "Rebirth" }
-- In <Name>_ServerHooks, above UI.Server.start. UI.Server already rate-limits the remote and checks
-- the shapes (elementId: a short string; payload: plain values). Everything else is yours to check.
local REBIRTH_COST = 10000

UI.Server.on("Rebirth", function(player, elementId, payload)
	-- a button sends no payload: ignore anything else a client made up
	if payload ~= nil then
		return
	end
	local coins = UI.Server.getCurrency(player, "coins") or 0
	if coins < REBIRTH_COST then
		return -- the server decides, never the client
	end
	UI.Server.setCurrency(player, "coins", 0)
	player:SetAttribute("Rebirths", (player:GetAttribute("Rebirths") or 0) + 1)
	-- TODO: save Rebirths with your own player data (UI.Server only saves the UI's currencies)
end)

Prefer your own RemoteEvent? Name it exactly like the action and put it in ReplicatedStorage. BloxUI fires it directly (you then lose UI.Server's rate limit and shape checks, so check everything yourself):

Server Script
-- Your own RemoteEvent works too: name it exactly like the action ("Rebirth") and put it in
-- ReplicatedStorage. BloxUI then fires it directly with (elementId, payload) instead of BloxUI_Action.
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Rebirth = Instance.new("RemoteEvent")
Rebirth.Name = "Rebirth"
Rebirth.Parent = ReplicatedStorage

Rebirth.OnServerEvent:Connect(function(player, elementId, payload)
	if typeof(elementId) ~= "string" or payload ~= nil then
		return
	end
	-- check and do the rebirth here, exactly like the UI.Server.on version
end)

Developer products and game passes#

1. Create the real ids#

A design comes with Robux prices but no ids. Until an item has one, pressing it says Not for sale yet! Publish your game, then open create.roblox.com/dashboard/creations > your experience > Monetization:

  • Developer Products for things bought again and again (currency packs, revives, spins).
  • Passes for things bought once (VIP, x2 coins, extra slots).

2. Put the ids in the design#

The ids belong in the blueprint. Each Robux item takes a productId or a gamePassId, and a currency pack also takes a grant so UI.Server can pay it by itself:

Two shop items in the blueprint
{ "id": "gems_small", "title": "Starter Gems", "icon": "gem", "price": 49, "currency": "robux",
  "productId": 3312847105, "grant": { "currency": "gems", "amount": 100 } }

{ "id": "vip", "title": "VIP", "icon": "crown", "price": 699, "currency": "robux",
  "gamePassId": 1203948871 }

The website has no id editor for the shop's buttons yet, so set them in the place with this Command Bar block. Items are matched by the title the shop shows. Run it again after you re-install the UI, because an install replaces the blueprint.

Command Bar: set ids, text and colours
-- Changes a UI's design (its blueprint) inside the place. Paste it into the Command Bar, change the
-- CHANGES part, press Enter, then press Play. Send to Studio / a new export replaces these edits.
local NAME = "NeonStrike" -- your UI's name: the StringValue in ReplicatedStorage > BloxUI_Blueprints

local HttpService = game:GetService("HttpService")
local folder = game:GetService("ReplicatedStorage"):FindFirstChild("BloxUI_Blueprints")
local value = folder and folder:FindFirstChild(NAME)
assert(value, "No UI named " .. NAME .. " in ReplicatedStorage.BloxUI_Blueprints")
local bp = HttpService:JSONDecode(value.Value)

local function each(t, fn) -- calls fn on every table inside the blueprint
	fn(t)
	for _, v in t do
		if type(v) == "table" then
			each(v, fn)
		end
	end
end

-- CHANGES -----------------------------------------------------------------------------------------
-- Robux items by the title the shop shows. Developer products: bought again and again.
local PRODUCTS = {
	["Starter Gems"] = 3312847105,
	["Small Gem Pack"] = 3312847188,
}
-- Game passes: bought once.
local PASSES = {
	["VIP"] = 1203948871,
}
each(bp, function(t)
	if t.price ~= nil and PRODUCTS[t.title] then
		t.productId, t.gamePassId = PRODUCTS[t.title], nil
	elseif t.price ~= nil and PASSES[t.title] then
		t.gamePassId, t.productId = PASSES[t.title], nil
	end
end)

bp.logo.text = "NEON STRIKE" -- the logo on the menu and loading screens
bp.style.palette.primary = "#FF4F7B" -- the main colour
-- -------------------------------------------------------------------------------------------------

value.Value = HttpService:JSONEncode(bp)
print("Saved " .. NAME .. ": press Play to see it")

Other ids live on their sections: premiumGamePassId or premiumProductId on a Pass (battle pass), luckGamePassId on a Spin, productId or gamePassId on an Offer screen, doubleProductId on offline earnings, groupId on Social rewards.

Using the Robux Products or Game Passes Extras? Their forms on the website take the ids and what each one gives, and the Extras deliver those purchases. The buttons still prompt the ids on their items in the blueprint, so use the same ids in both places (Robux Products warns in Output when they differ).

3. Delivering what was bought#

  • Items with a grant (currency packs, starter packs, premium passes): UI.Server answers Roblox's ProcessReceipt, pays the grant and saves it together with the purchase id before it tells Roblox the purchase is done. Nothing to write.
  • Products without a grant (a revive, a pet): ServerHooks has a UI.Server.onReceipt(productId, fn) stub for each. You give the thing.
  • Game passes: ServerHooks checks ownership with Roblox on join and after a purchase, then sets the player attribute GamePass_<item id>. Your code reads that attribute.
ServerHooks: a product without a grant
-- "Revive" (25 Robux) in Shop > Extras: a developer product without a grant.
-- This is the stub Export as code writes; you fill in the TODO.
UI.Server.onReceipt(3312847230, function(player: Player, receiptInfo: any): boolean
	local claimId = "product:" .. tostring(receiptInfo.PurchaseId)
	if claimId and alreadyGiven(player, claimId) then
		return true -- given before (a retry): never give twice
	end
	local given = false
	-- TODO: give what it contains, then set given = true
	--   e.g.  given = revivePlayer(player)   (your function, true when it worked)
	if given and claimId then
		remember(player, claimId) -- keep it with what you gave
	end
	return given -- false: not granted yet, Roblox tries again later
end)
ServerHooks: game passes
-- ServerHooks (written by Export as code): owners get the attribute GamePass_<item id>
local GAME_PASSES: { [number]: string } = {
	[1203948871] = "vip", -- "VIP" (299 Robux) in Shop > Passes
}

local function givePass(player: Player, gamePassId: number)
	player:SetAttribute("GamePass_" .. GAME_PASSES[gamePassId], true)
	-- TODO: turn the perk on here (or read the GamePass_<id> attribute where it applies)
end

-- Anywhere on the server: is this player VIP?
local function isVip(player: Player): boolean
	return player:GetAttribute("GamePass_vip") == true
end

-- A VIP-only door: the server lets owners through
workspace:WaitForChild("VipDoor").Touched:Connect(function(hit)
	local player = game:GetService("Players"):GetPlayerFromCharacter(hit.Parent)
	if player and isVip(player) and player.Character then
		player.Character:PivotTo(workspace.VipRoom.Entrance.CFrame + Vector3.new(0, 3, 0))
	end
end)

Only one script in a game may set MarketplaceService.ProcessReceipt. If your game already has one, start with ProcessReceipt = false and call UI.Server.handleReceipt(receiptInfo) from yours (see Saving data). BloxUI never trusts the purchase prompt's "purchased" flag for game passes: it asks Roblox (UserOwnsGamePassAsync). If a test purchase doesn't turn a pass on in Studio, check it in the live game.

Saving data#

With SaveData = true (what ServerHooks picks when the UI keeps anything per player), UI.Server saves the UI's currencies, upgrade levels, premium passes, reward claims and cooldowns, and the last 100 purchase ids, in the DataStore BloxUI_Data (key u_<userId>). It loads on join, saves on change (at most once a minute), when the player leaves and when the server shuts down, and locks each player's data to one server at a time so teleports and rejoins never lose coins. Settings are saved separately in BloxUI_Settings.

ServerHooks: the start options
-- The last lines of <Name>_ServerHooks. Pick one.

-- 1. BloxUI saves the UI's data (the usual choice): currencies, upgrade levels, premium passes,
--    reward claims and purchase receipts, in the DataStore "BloxUI_Data", one server at a time.
UI.Server.start({ SaveData = true })

-- 2. Your own data code saves the currencies. Purchases then wait until your save worked.
UI.Server.start({
	SaveData = false,
	saveHook = function(player)
		return MyData.save(player) -- your function: true once the player's data is saved
	end,
})

-- 3. Your game already sets MarketplaceService.ProcessReceipt: keep yours, and call BloxUI from it.
UI.Server.start({ SaveData = true, ProcessReceipt = false })
-- ...then, inside your own ProcessReceipt:
--   local decision = UI.Server.handleReceipt(receiptInfo)

-- Fractions or very big numbers in a leaderstat currency
UI.Server.start({ SaveData = true, NumberValues = true })
  • Studio: DataStores need a published game and Game Settings > Security > Enable Studio Access to API Services. Without it, data lives in memory for the session only.
  • Your own data code: a leaderstat your scripts create before the player joins is left alone, unless you start with SaveData = true. With SaveData = false, nothing is saved by BloxUI and Robux purchases are only granted once your saveHook says your save worked.
  • UI.Server saves only what the UI owns. Your own progress (levels, inventory, quest progress) still needs your own DataStore code, or the Extras: their data travels in the same save as the UI's, one server at a time.

Settings#

Settings screens are real: each change applies at once, fires ui:on(id), and is saved per player (sent to the server 2 seconds after the last change). There are two kinds:

Built-in (BloxUI applies them)What they change
sfx, musicUI sound and music volume or on / off
fov, sensitivity, shadowscamera field of view, mouse sensitivity, Lighting.GlobalShadows (this player only)
lowDetail, reducedMotion, colorblind, uiScalelite mode, calmer effects, colour-blind palettes, UI size
damageNumbers, hitMarkers, killFeed, notifications, skipHatch, showHudthe matching HUD and effect switches (F1 always brings a hidden HUD back)

Event-only settings are anything else (showOtherPets, aimAssist, speedUnits...). BloxUI shows and saves them; only your code can apply them, in their ui:on handler:

UIHooks: an event-only setting
-- UIHooks (client). An event-only setting: BloxUI shows and saves it, your code applies it.
ui:on("showOtherPets", function(context)
	local show = context.payload -- true / false
	local me = game:GetService("Players").LocalPlayer.UserId
	for _, pet in workspace:WaitForChild("Pets"):GetChildren() do
		if pet:GetAttribute("OwnerId") ~= me then -- your game's own way to tell whose pet it is
			for _, part in pet:GetDescendants() do
				if part:IsA("BasePart") then
					part.LocalTransparencyModifier = if show then 0 else 1 -- only on this screen
				end
			end
		end
	end
end)

-- Saved values load before your script runs: Export as code adds this so every handler starts right
for _, id in { "showOtherPets" } do
	ui:emit(id, { id = id, payload = ui:getSetting(id), source = "start" })
end

-- Read or change any setting yourself (built-in ones apply at once and are saved too)
print(ui:getSetting("fov")) --> 70
ui:setSetting("music", 30) -- false when the value isn't allowed

On the server, UI.Server.getSettings(player) returns every setting with the defaults filled in.

Game systems#

Some sections run entirely on the server; others only show what your game tells them. The hook scripts' comments say which is which.

SystemWho runs itWhat you add
Shop items priced in coins or gems, upgrades, cases / eggsUI.Server: checks the price on the server, takes it, pays the grant, rolls casesBought:<id> handlers for items without a grant, onUpgrade, onCaseDrop
Spin wheel, playtime gifts, Index milestones, achievements, social rewardsUI.Server: the server's clock, dice and checksonSpinPrize / onReward for prizes without a grant; progress for achievements and the Index
Limited-time offers, offline earningsUI.Server (shown after joining, bought through the usual prompts)ids; onReward for offer items without a grant
Daily rewards, quests, battle pass tiersDisplay onlyyour server keeps the progress and gives the reward
Codes, teleports, trading, rebirthYour servera Remote or Event handler

Daily rewards#

The Daily window has no clock: it lets the player press the next day and fires DailyClaim. Your server decides, and the window shows the count your server sends back.

ServerHooks: daily rewards
-- ServerHooks, above UI.Server.start. The server keeps the streak; the window only shows it.
-- Make a RemoteEvent named "ClaimDaily" in ReplicatedStorage first.
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local store = DataStoreService:GetDataStore("DailyRewards")
local ClaimDaily = ReplicatedStorage:WaitForChild("ClaimDaily")

local REWARDS = { 50, 75, 100, 150, 200, 300, 500 } -- coins for days 1-7: match your Daily section
local WAIT = 20 * 3600 -- the next day opens 20 hours after the last claim
local RESET = 48 * 3600 -- 48 hours without a claim starts the streak again

local data = {} -- [player] = { streak, last }

local function status(d)
	local since = os.time() - d.last
	local streak = if since >= RESET then 0 else d.streak
	return streak % #REWARDS, since >= WAIT -- days claimed this week, is the next one ready
end

local function onJoin(player: Player)
	local ok, saved = pcall(function()
		return store:GetAsync("u_" .. player.UserId)
	end)
	if not ok or player.Parent == nil then
		return -- DataStore trouble: no claims this visit, and nothing good is overwritten
	end
	data[player] = if type(saved) == "table" then saved else { streak = 0, last = 0 }
	ClaimDaily:FireClient(player, (status(data[player])))
end
Players.PlayerAdded:Connect(onJoin)
for _, player in Players:GetPlayers() do
	task.spawn(onJoin, player)
end

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

ClaimDaily.OnServerEvent:Connect(function(player)
	local d = data[player]
	if d == nil or d.busy then
		return
	end
	local claimed, ready = status(d)
	if not ready then
		ClaimDaily:FireClient(player, claimed, "Come back tomorrow for the next reward!")
		return
	end
	d.busy = true
	local streak = (if os.time() - d.last >= RESET then 0 else d.streak) + 1
	local new = { streak = streak, last = os.time() }
	local saved = pcall(function()
		store:SetAsync("u_" .. player.UserId, new)
	end)
	d.busy = nil
	if not saved then
		ClaimDaily:FireClient(player, claimed, "Your reward couldn't be saved. Try again in a minute.")
		return
	end
	data[player] = new
	local day = (streak - 1) % #REWARDS + 1
	UI.Server.grant(player, "coins", REWARDS[day])
	ClaimDaily:FireClient(player, day)
end)
UIHooks: daily rewards
-- UIHooks (client). The Daily window lets the player press the next day at any time: the server
-- answers with the real count, and the window follows it.
local ClaimDaily = game:GetService("ReplicatedStorage"):WaitForChild("ClaimDaily")
local DAILY = "daily" -- the Daily section's id (see the Examples table at the bottom of UIHooks)

ui:on("DailyClaim", function(context)
	ClaimDaily:FireServer() -- context.payload is the day pressed; the server decides anyway
end)

ClaimDaily.OnClientEvent:Connect(function(claimed, message)
	ui:state(DAILY, "claimed", 0):set(claimed)
	if message then
		ui:notify(message, "info")
	end
end)

Quests#

A quest shows the progress in ui:state(<section id>, "<quest id>.progress") and its Claim button lights up at the goal. Keep the progress on the server, mirror it with attributes, and check claims there:

ServerHooks: quests
-- ServerHooks, above UI.Server.start. Make a RemoteEvent named "ClaimQuest" in ReplicatedStorage.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ClaimQuest = ReplicatedStorage:WaitForChild("ClaimQuest")

-- match the ids, goals and rewards of your Quests section
local QUESTS = {
	q1 = { goal = 10, coins = 100 }, -- Hatch 10 eggs
	q2 = { goal = 50, coins = 250 }, -- Collect 50 gems
}

-- call this from your game code, e.g. addQuestProgress(player, "q1", 1) when an egg hatches
local function addQuestProgress(player: Player, questId: string, amount: number)
	local q = QUESTS[questId]
	if q then
		local now = player:GetAttribute("Quest_" .. questId) or 0
		player:SetAttribute("Quest_" .. questId, math.min(q.goal, now + amount))
	end
end

ClaimQuest.OnServerEvent:Connect(function(player, questId)
	local q = typeof(questId) == "string" and QUESTS[questId]
	if not q or player:GetAttribute("QuestDone_" .. questId) then
		return
	end
	if (player:GetAttribute("Quest_" .. questId) or 0) < q.goal then
		return -- not finished: the client can't claim early
	end
	player:SetAttribute("QuestDone_" .. questId, true)
	UI.Server.grant(player, "coins", q.coins)
	-- TODO: save quest progress and QuestDone_ with your player data if quests should last past a rejoin
end)
UIHooks: quests
-- UIHooks (client): the Quests window follows the attributes the server sets
local Players = game:GetService("Players")
local ClaimQuest = game:GetService("ReplicatedStorage"):WaitForChild("ClaimQuest")
local QUESTS = "quests" -- the Quests section's id
local player = Players.LocalPlayer

for _, questId in { "q1", "q2" } do
	local function sync()
		ui:state(QUESTS, questId .. ".progress", 0):set(player:GetAttribute("Quest_" .. questId) or 0)
		ui:state(QUESTS, questId .. ".claimed", false):set(player:GetAttribute("QuestDone_" .. questId) == true)
	end
	player:GetAttributeChangedSignal("Quest_" .. questId):Connect(sync)
	player:GetAttributeChangedSignal("QuestDone_" .. questId):Connect(sync)
	sync()
end

ui:on("QuestClaim", function(context)
	ClaimQuest:FireServer(context.payload) -- the quest id
end)

Battle pass#

The tier reached is ui:state(<section id>, "progress"). Premium is automatic: UI.Server sets BloxUI_Premium_<section id> for owners of the Pass's premiumGamePassId or buyers of its premiumProductId, and the window follows it. Tier rewards are yours to give:

ServerHooks: battle pass claims
-- Battle pass. Server (ServerHooks): your game raises the tier; claims are checked here.
-- Make a RemoteEvent named "ClaimPassTier" in ReplicatedStorage.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ClaimPassTier = ReplicatedStorage:WaitForChild("ClaimPassTier")
local PASS = "pass" -- the Pass section's id
local FREE = { [1] = 50, [2] = 100, [3] = 150 } -- coins per tier: match your Pass section
local PREMIUM = { [1] = 20, [2] = 40, [3] = 80 } -- gems per tier

ClaimPassTier.OnServerEvent:Connect(function(player, tier, track)
	if typeof(tier) ~= "number" or (track ~= "free" and track ~= "premium") then
		return
	end
	if tier > (player:GetAttribute("PassTier") or 0) then
		return -- not reached yet
	end
	-- UI.Server sets BloxUI_Premium_<section id> for owners of the premium product or game pass
	if track == "premium" and player:GetAttribute("BloxUI_Premium_" .. PASS) ~= true then
		return
	end
	local key = "PassClaimed_" .. track .. "_" .. tier
	if player:GetAttribute(key) then
		return
	end
	player:SetAttribute(key, true) -- TODO: save claims and PassTier with your player data
	if track == "free" then
		UI.Server.grant(player, "coins", FREE[tier] or 0)
	else
		UI.Server.grant(player, "gems", PREMIUM[tier] or 0)
	end
end)
UIHooks: battle pass
-- Battle pass. Client (UIHooks): show the tier reached, send claims to the server
local ClaimPassTier = game:GetService("ReplicatedStorage"):WaitForChild("ClaimPassTier")
local PASS = "pass" -- the Pass section's id
local player = game:GetService("Players").LocalPlayer
local function syncTier()
	ui:state(PASS, "progress", 0):set(player:GetAttribute("PassTier") or 0)
end
player:GetAttributeChangedSignal("PassTier"):Connect(syncTier)
syncTier()

ui:on("PassClaim", function(context)
	ClaimPassTier:FireServer(context.payload.tier, context.payload.track)
end)

Codes#

The Codes box's action is a Remote (usually RedeemCode) and the typed code is the payload. ServerHooks writes the handler with a code table that only the server sees:

ServerHooks: codes
-- The Codes box's action: { "kind": "Remote", "name": "RedeemCode" }. Export as code writes this.
do
	-- The codes and what each one gives, here on the server only (a client never sees them).
	local CODES: { [string]: number } = {
		RELEASE = 50,
		THANKS100K = 250,
	}
	-- Codes each player used. TODO: save this with the player's data, or a code works
	-- once per server instead of once per player.
	local used: { [number]: { [string]: boolean } } = {}

	UI.Server.on("RedeemCode", function(player: Player, elementId: any, payload: any)
		-- Accept a short string only, then look it up in CODES.
		if typeof(payload) ~= "string" or #payload > 40 then
			return
		end
		local code = string.upper((string.gsub(payload, "%s+", "")))
		local amount = CODES[code]
		local mine = used[player.UserId] or {}
		if amount == nil or mine[code] then
			return -- unknown, or used already
		end
		mine[code] = true
		used[player.UserId] = mine
		UI.Server.grant(player, "gems", amount) -- Gems (saved)
	end)
end

Spin wheel, gifts, Index, achievements and offers#

UI.Server decides every one of these: the wheel is rolled with the server's random numbers, free spins and gifts use the server's clock, group rewards ask Roblox, invites are checked. Prizes with a grant are paid automatically. The rest go to these hooks, idempotent by claim id like onReceipt:

ServerHooks: delivery hooks
-- ServerHooks. Prizes WITH a grant ({ currency, amount }) are paid by UI.Server itself.
-- Prizes without one (a pet, a boost...) come to these hooks. Return true only once it's given.

-- Spin wheel wedges: wedge = { id, label, icon, amount, rarity }; info = { section, index, spinId, mode, purchaseId? }
UI.Server.onSpinPrize(function(player: Player, wedge: any, info: any): boolean
	local claimId = "spin:" .. tostring(info.spinId)
	if claimId and alreadyGiven(player, claimId) then
		return true -- given before (a retry): never give twice
	end
	local given = false
	-- TODO: give the wedge's prize, then set given = true
	if given and claimId then
		remember(player, claimId)
	end
	return given -- false: the spin is undone (cooldown or price given back)
end)

-- Gifts, Index milestones, achievements, social rewards and offer items without a grant
-- info = { kind = "Gift" | "Index" | "Achievement" | "Social" | "Offer", section, id, claimId, purchaseId? }
UI.Server.onReward(function(player: Player, reward: any, info: any): boolean
	local claimId = info.claimId
	if claimId and alreadyGiven(player, claimId) then
		return true
	end
	local given = false
	-- TODO: give the reward (reward.label, reward.icon, reward.amount), then set given = true
	if given and claimId then
		remember(player, claimId)
	end
	return given -- false: the claim fails and can be tried again (an offer purchase waits)
end)

-- Case / egg drops: drop = { name, icon, rarity, chance }; info = { case, section, purchaseId? }
UI.Server.onCaseDrop(function(player: Player, drop: any, info: any): boolean
	local claimId = if info.purchaseId then "case:" .. tostring(info.purchaseId) else nil
	if claimId and alreadyGiven(player, claimId) then
		return true
	end
	local given = false
	-- TODO: give drop.name to the player (your inventory), then set given = true
	if given and claimId then
		remember(player, claimId)
	end
	return given -- false: an in-game buy is refunded, a Robux one retried later
end)

Achievements, the Index and upgrades read what your game sets on the server:

ServerHooks: progress the UI reads
-- ServerHooks. Achievements, the Index and upgrades read what your game code sets on the server.

-- Achievements: progress is the player attribute BloxUI_Ach_<achievement id>.
-- The player can claim the reward once it reaches the goal (UI.Server checks it).
local function addAchievementProgress(player: Player, achievementId: string, amount: number)
	local attribute = "BloxUI_Ach_" .. achievementId
	local now = player:GetAttribute(attribute)
	player:SetAttribute(attribute, (if typeof(now) == "number" then now else 0) + amount)
end
-- e.g. addAchievementProgress(player, "ach_first_hatch", 1) when a player hatches an egg

-- Index (collection book): tell UI.Server what the player has found, on join and whenever
-- they find something new. Milestones pay from it.
local function updateIndex(player: Player, foundIds: { string })
	UI.Server.setIndexOwned(player, foundIds) -- e.g. { "dex_goldfish", "dex_parrot" }
end

-- Upgrades: BloxUI sells the levels (player attribute BloxUI_Upgrade_<id>, saved). The generated
-- onUpgrade runs on join and after every level bought: apply the level there.
local function onUpgrade(player: Player, id: string, level: number)
	if id == "walk" then -- "Walk Speed: +10% speed"
		local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
		if humanoid then
			humanoid.WalkSpeed = 16 * (1 + 0.1 * level)
		end
	end
end

Teleports#

Teleport rows and Worlds cards fire the Event Teleport with the place in context.payload. The game moves the player, on the server, after checking:

UIHooks
-- UIHooks (client). Teleport rows and Worlds cards fire the Event "Teleport" with the place in context.payload.
local TeleportTo = game:GetService("ReplicatedStorage"):WaitForChild("TeleportTo") -- your RemoteEvent

ui:on("Teleport", function(context)
	-- context.payload = "spawn" | "shop" | "eggs" | "vip" (the button id is context.id, e.g. "tp_spawn")
	local place = context.payload or string.match(context.id or "", "^tp_(.+)$")
	TeleportTo:FireServer(place)
end)
ServerHooks
-- ServerHooks (server). A client can ask for any place, so the server checks before it moves anyone.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TeleportTo = Instance.new("RemoteEvent")
TeleportTo.Name = "TeleportTo"
TeleportTo.Parent = ReplicatedStorage

local PLACES = {
	spawn = workspace.Spawns.Main,
	shop = workspace.Zones.Shop,
	eggs = workspace.Zones.Eggs,
	vip = workspace.Zones.VIP,
}

TeleportTo.OnServerEvent:Connect(function(player, place)
	local target = typeof(place) == "string" and PLACES[place]
	if target and player.Character and (place ~= "vip" or player:GetAttribute("GamePass_vip")) then
		player.Character:PivotTo(target.CFrame + Vector3.new(0, 3, 0))
	end
end)

Trading#

The Trade window is only the screen. Offers, accepting and swapping items are your game's code, checked on the server:

UIHooks: trade buttons
-- UIHooks (client). The Trade window is the screen only: offers, accepting and swapping are yours.
local Trade = game:GetService("ReplicatedStorage"):WaitForChild("Trade") -- your RemoteEvent

ui:on("TradeAccept", function(context)
	Trade:FireServer("accept") -- the server checks BOTH offers again, then swaps the items itself
end)

ui:on("TradeDecline", function(context)
	Trade:FireServer("decline")
end)

The checklist#

The top of both hook scripts lists what's left to do, in groups. Red groups break something until fixed; the rest are advice.

GroupMeansFix
Product and game-pass idsRobux items, cases, spins, premium passes, x2 Luck, offers and x2 offline earnings without a real id, or with an id that looks made up (under 1000, repeated digits, a counting sequence, a round number, or within 9 of another id)create the ids and add them, then make the hook scripts again
Group id for Social rewardsa "Join the Group" reward without your group's id: nobody can claim itset groupId (the number in your group's address)
Items without a grantthings BloxUI can't give by itself, each with the handler that must: onReceipt, givePass, Bought:<id>, onCaseDrop, onSpinPrize, onReward, onUpgrade, display-only Daily / Quests / Pass rewardsfill in those handlers
Event-only settingssettings BloxUI saves but can't applyfill in their ui:on handlers
Empty sound slotssounds that use the theme's defaults (a note, not a problem)optional: your own Creator Store audio ids in style.sounds and music
Validator warningswhat BloxUI repaired when it read the design, and Remote actions named like server eventsfix the design so it is exactly what you meant

Quick reference#

Client (UIHooks)
UI.Blueprint.waitFor(name, seconds)the built UI, or nil after the timeout
ui:on(name, fn) / ui:emit(name, context)listen to / fire an event; ui:on returns a function that disconnects
ui:open(id) ui:close(id) ui:toggle(id) ui:isOpen(id) ui:closeAll()screens
ui.currencies[id], UI.peek(value)a currency's Fusion value, and its number
ui:give(id, amount, from?)the fly-in (only the effect for server-owned currencies)
ui:notify(text, kind?, essential?)a toast; essential shows even with notifications off
ui:getSetting(id) / ui:setSetting(id, value)settings
ui:state(sectionId, key, default)a section's shown state (daily claimed, quest <id>.progress, pass progress)
ui.hud[elementId]HUD element handles (see the Examples table)
ui:setOwned(ids)what an Index shows as found, display only (with UI.Server use setIndexOwned)
Server (ServerHooks)
UI.Server.start(options)last line; SaveData, saveHook, ProcessReceipt, NumberValues, SaveSettings
UI.Server.on(name, fn(player, elementId, payload))Remote actions and server events
UI.Server.grant(player, id, amount), getCurrency, setCurrencycurrencies
UI.Server.onReceipt(productId, fn)products without a grant: return true once given
UI.Server.onCaseDrop, onSpinPrize, onRewardprizes without a grant: return true once given
UI.Server.canBuy = fn(player, sectionId, itemId)veto an in-game-currency buy (return false)
UI.Server.setIndexOwned(player, ids)what the player has found (Index)
UI.Server.getSettings(player), UI.Server.handleReceipt(info), UI.Server.flush()settings, your own ProcessReceipt, save now

Next: let an AI write the handlers for you in Vibe-code it with Claude or ChatGPT.

© 2026 BloxMaps · bloxmaps.com · Terms · Privacy · Contact

BloxMaps is not affiliated with, endorsed by or sponsored by Roblox Corporation. Roblox and Roblox Studio are trademarks of Roblox Corporation.