Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/brockmartin/roblox-game-skill/llms.txt

Use this file to discover all available pages before exploring further.

All GUI code in Roblox runs on the client inside LocalScripts. UI objects live under StarterGui at edit time and are cloned into each player’s PlayerGui at runtime. The client owns presentation — responsive layout, animation, input handling — but the server owns all game logic. Never let the client make purchase decisions, grant items, or modify player state directly from a UI event.

GUI Containers

ScreenGui (2D Overlay)

The primary container for all 2D interface elements. Placed in StarterGui; Roblox copies it into each player’s PlayerGui on spawn.
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")

local screenGui = Instance.new("ScreenGui")
screenGui.Name = "MainHUD"
screenGui.ResetOnSpawn = false -- survives respawn
screenGui.DisplayOrder = 10   -- higher renders on top
screenGui.IgnoreGuiInset = true -- extends behind top bar
screenGui.Parent = playerGui
PropertyPurpose
DisplayOrderControls layering. Higher values render on top.
ResetOnSpawntrue (default): destroyed and re-cloned on respawn. Set false for persistent UI (shops, settings).
IgnoreGuiInsettrue: UI extends behind the top bar area. Use for fullscreen overlays.
EnabledToggle visibility without destroying.

BillboardGui (Floating 3D)

Always faces the camera. Used for nametags, damage numbers, and quest markers.
local billboardGui = Instance.new("BillboardGui")
billboardGui.Size = UDim2.new(0, 200, 0, 50)
billboardGui.StudsOffset = Vector3.new(0, 3, 0) -- above the part
billboardGui.AlwaysOnTop = false -- occluded by 3D geometry
billboardGui.MaxDistance = 100   -- hides beyond this range
billboardGui.Adornee = workspace.NPC.Head
billboardGui.Parent = workspace.NPC.Head

SurfaceGui (On Part Surfaces)

Renders UI on a Part’s surface. Used for in-world signs, screens, and control panels.
local surfaceGui = Instance.new("SurfaceGui")
surfaceGui.Face = Enum.NormalId.Front
surfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
surfaceGui.PixelsPerStud = 50
surfaceGui.Parent = workspace.SignPart
-- Also set surfaceGui.Adornee = workspace.SignPart if parented elsewhere

ViewportFrame (3D Content Inside 2D UI)

Renders 3D content inside a 2D GUI element. Used for item previews, character displays, and trophy cases.
local viewport = Instance.new("ViewportFrame")
viewport.Size = UDim2.new(0, 200, 0, 200)
viewport.BackgroundColor3 = Color3.fromRGB(20, 20, 20)
viewport.Parent = frame

-- Clone a model into the viewport
local previewModel = workspace.SwordModel:Clone()
previewModel.Parent = viewport

-- Add a camera
local camera = Instance.new("Camera")
camera.CFrame = CFrame.new(Vector3.new(0, 2, 5), Vector3.new(0, 1, 0))
camera.Parent = viewport
viewport.CurrentCamera = camera

Display Order Hierarchy

DisplayOrder 100  -- Modal dialogs (on top of everything)
DisplayOrder 50   -- Notifications / toasts
DisplayOrder 10   -- HUD elements
DisplayOrder 1    -- Background UI

Core UI Elements

Basic Building Blocks

-- Frame: container for grouping and styling
local frame = Instance.new("Frame")
frame.Size = UDim2.new(0.3, 0, 0.4, 0)         -- 30% width, 40% height
frame.Position = UDim2.new(0.5, 0, 0.5, 0)      -- centered
frame.AnchorPoint = Vector2.new(0.5, 0.5)
frame.BackgroundColor3 = Color3.fromRGB(30, 30, 40)
frame.BackgroundTransparency = 0.1
frame.BorderSizePixel = 0
frame.Parent = screenGui

-- TextButton: cross-platform interactive element
local button = Instance.new("TextButton")
button.Size = UDim2.new(0.5, 0, 0, 50)
button.Text = "Purchase"
button.TextColor3 = Color3.fromRGB(255, 255, 255)
button.BackgroundColor3 = Color3.fromRGB(0, 170, 80)
button.Font = Enum.Font.GothamBold
button.TextSize = 18
button.Parent = frame

button.Activated:Connect(function()
    -- Activated works for mouse click, touch tap, and gamepad
end)
Always use Activated instead of MouseButton1Click on buttons. Activated fires on mouse click, touch tap, and gamepad A/B, giving you cross-platform support for free.

Layout Modifiers

ModifierPurpose
UIListLayoutArranges children in a vertical or horizontal list
UIGridLayoutArranges children in a grid
UIPageLayoutSwipeable pages (one child visible at a time)
UIPaddingInner padding on a container
UICornerRounded corners
UIStrokeOutline/border effect
UIGradientColor gradient on an element
UISizeConstraintMin/max pixel size
UIAspectRatioConstraintLocks width/height ratio
-- Rounded corners + stroke + gradient in one block
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 8)
corner.Parent = frame

local stroke = Instance.new("UIStroke")
stroke.Color = Color3.fromRGB(255, 255, 255)
stroke.Thickness = 2
stroke.Transparency = 0.5
stroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
stroke.Parent = frame

local gradient = Instance.new("UIGradient")
gradient.Color = ColorSequence.new(
    Color3.fromRGB(50, 50, 80),
    Color3.fromRGB(20, 20, 40)
)
gradient.Rotation = 90
gradient.Parent = frame

Layout Systems

When to Use Each

ScenarioLayout
Vertical menu, chat log, leaderboardUIListLayout
Inventory grid, shop items, emoji pickerUIGridLayout
Settings tabs, tutorial slides, shop categoriesUIPageLayout
HUD element pinned to a cornerAbsolute positioning (no layout)
Overlapping elements (health bar segments)Absolute positioning

UIListLayout

local listLayout = Instance.new("UIListLayout")
listLayout.FillDirection = Enum.FillDirection.Vertical
listLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
listLayout.VerticalAlignment = Enum.VerticalAlignment.Top
listLayout.SortOrder = Enum.SortOrder.LayoutOrder
listLayout.Padding = UDim.new(0, 8) -- gap between items
listLayout.Parent = frame
Children are sorted by their LayoutOrder property (lower values first).

UIGridLayout

local gridLayout = Instance.new("UIGridLayout")
gridLayout.CellSize = UDim2.new(0, 80, 0, 80)
gridLayout.CellPadding = UDim2.new(0, 8, 0, 8)
gridLayout.FillDirection = Enum.FillDirection.Horizontal
gridLayout.FillDirectionMaxCells = 4 -- 4 columns, then wrap
gridLayout.SortOrder = Enum.SortOrder.LayoutOrder
gridLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
gridLayout.Parent = scrollFrame

UIPageLayout

local pageLayout = Instance.new("UIPageLayout")
pageLayout.Animated = true
pageLayout.EasingStyle = Enum.EasingStyle.Quad
pageLayout.EasingDirection = Enum.EasingDirection.InOut
pageLayout.TweenTime = 0.3
pageLayout.Circular = false
pageLayout.Parent = frame

-- Navigate pages
pageLayout:JumpToIndex(0)
pageLayout:Next()
pageLayout:Previous()

Responsive Design

Scale vs Offset

UDim2 has two components: Scale (proportion of parent, 0–1) and Offset (fixed pixels).
-- BAD: hardcoded pixels, breaks on different screens
frame.Size = UDim2.new(0, 400, 0, 300)
frame.Position = UDim2.new(0, 100, 0, 50)

-- GOOD: proportional, adapts to any screen
frame.Size = UDim2.new(0.3, 0, 0.4, 0)
frame.Position = UDim2.new(0.5, 0, 0.5, 0)
frame.AnchorPoint = Vector2.new(0.5, 0.5)
Rule: Use Scale for Position and Size. Use Offset only for small fixed elements (icons, padding, border thickness).

Adapting to Screen Size

local camera = workspace.CurrentCamera

local function adaptUI()
    local viewportSize = camera.ViewportSize
    local isPortrait = viewportSize.Y > viewportSize.X
    local isSmallScreen = viewportSize.X < 600

    if isSmallScreen then
        frame.Size = UDim2.new(0.95, 0, 0.8, 0)  -- nearly fullscreen on mobile
    elseif isPortrait then
        frame.Size = UDim2.new(0.7, 0, 0.5, 0)
    else
        frame.Size = UDim2.new(0.3, 0, 0.4, 0)   -- standard desktop
    end
end

camera:GetPropertyChangedSignal("ViewportSize"):Connect(adaptUI)
adaptUI()

Animation with TweenService

Common Easing Styles

StyleUse Case
QuadGeneral-purpose, smooth
BackSlight overshoot — bouncy buttons
ElasticSpringy, attention-grabbing
BounceBouncing effect at the end
LinearConstant speed — progress bars
SineGentle, subtle motion

Core Animation Recipes

local TweenService = game:GetService("TweenService")

-- Slide in from the right
frame.Position = UDim2.new(1.5, 0, 0.5, 0)

local slideIn = TweenService:Create(frame,
    TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
    Position = UDim2.new(0.5, 0, 0.5, 0),
})
slideIn:Play()

-- Bounce entrance (scale from zero)
local function bounceIn(element: GuiObject)
    element.Size = UDim2.new(0, 0, 0, 0)
    element.AnchorPoint = Vector2.new(0.5, 0.5)
    local tween = TweenService:Create(element,
        TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
        Size = UDim2.new(0.3, 0, 0.4, 0),
    })
    tween:Play()
    return tween
end

-- Fade + scale dismiss
local function dismiss(element: GuiObject): RBXScriptSignal
    local tween = TweenService:Create(element,
        TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
        Size = UDim2.new(0, 0, 0, 0),
        BackgroundTransparency = 1,
    })
    tween:Play()
    return tween.Completed
end

Chaining Tweens

Use the Completed event to sequence tweens without coroutines:
local step1 = TweenService:Create(frame, TweenInfo.new(0.3), {
    Position = UDim2.new(0.5, 0, 0.5, 0),
})
local step2 = TweenService:Create(frame, TweenInfo.new(0.2), {
    Size = UDim2.new(0.4, 0, 0.5, 0),
})

step1.Completed:Connect(function()
    step2:Play()
end)
step1:Play()

Input Handling

UserInputService vs ContextActionService

ServiceBest For
UserInputServiceGlobal hotkeys, mouse tracking, detecting input device type, custom cursor
ContextActionServiceIn-game actions that need mobile buttons, context-sensitive controls
GuiButton.ActivatedUI button clicks (already cross-platform)
local UserInputService = game:GetService("UserInputService")

-- Keyboard shortcuts
UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessed: boolean)
    if gameProcessed then return end -- ignore when typing in TextBox etc.

    if input.KeyCode == Enum.KeyCode.E then
        toggleInventory()
    elseif input.KeyCode == Enum.KeyCode.Escape then
        togglePauseMenu()
    end
end)

-- Detect platform
local isMobile = UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
local isConsole = UserInputService.GamepadEnabled
local ContextActionService = game:GetService("ContextActionService")

local function onInteract(actionName: string, inputState: Enum.UserInputState, inputObject: InputObject)
    if inputState == Enum.UserInputState.Begin then
        interactWithNearestObject()
    end
    return Enum.ContextActionResult.Sink
end

-- Bind to E key; auto-creates touch button on mobile
ContextActionService:BindAction("Interact", onInteract, true, Enum.KeyCode.E)
ContextActionService:SetPosition("Interact", UDim2.new(0.8, 0, 0.5, 0))
ContextActionService:SetTitle("Interact", "E")

Complete Health Bar

A smooth health bar with color transitions, a trailing damage indicator, and a low-health pulse effect.
-- LocalScript in StarterPlayerScripts
local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")

local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")

-- Build the container
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "HealthBarGui"
screenGui.ResetOnSpawn = true
screenGui.DisplayOrder = 5
screenGui.Parent = playerGui

local container = Instance.new("Frame")
container.Name = "HealthBarContainer"
container.Size = UDim2.new(0.25, 0, 0, 28)
container.Position = UDim2.new(0.5, 0, 0.92, 0)
container.AnchorPoint = Vector2.new(0.5, 0.5)
container.BackgroundColor3 = Color3.fromRGB(20, 20, 20)
container.BorderSizePixel = 0
container.Parent = screenGui

Instance.new("UICorner", container).CornerRadius = UDim.new(0, 6)

-- Damage flash layer (red, shrinks slowly behind the fill)
local damageBar = Instance.new("Frame")
damageBar.Name = "DamageBar"
damageBar.Size = UDim2.new(1, 0, 1, 0)
damageBar.BackgroundColor3 = Color3.fromRGB(200, 50, 50)
damageBar.BorderSizePixel = 0
damageBar.ZIndex = 1
damageBar.Parent = container

-- Main health fill
local fillBar = Instance.new("Frame")
fillBar.Name = "FillBar"
fillBar.Size = UDim2.new(1, 0, 1, 0)
fillBar.BackgroundColor3 = Color3.fromRGB(0, 200, 80)
fillBar.BorderSizePixel = 0
fillBar.ZIndex = 2
fillBar.Parent = container

-- Color thresholds
local COLOR_HIGH   = Color3.fromRGB(0, 200, 80)
local COLOR_MEDIUM = Color3.fromRGB(230, 180, 0)
local COLOR_LOW    = Color3.fromRGB(200, 40, 40)

local function getHealthColor(fraction: number): Color3
    if fraction > 0.6 then
        return COLOR_HIGH
    elseif fraction > 0.3 then
        local t = (fraction - 0.3) / 0.3
        return COLOR_MEDIUM:Lerp(COLOR_HIGH, t)
    else
        local t = fraction / 0.3
        return COLOR_LOW:Lerp(COLOR_MEDIUM, t)
    end
end

local currentTween: Tween? = nil
local damageTween: Tween? = nil

local function updateHealthBar(health: number, maxHealth: number)
    local fraction = math.clamp(health / maxHealth, 0, 1)

    if currentTween then currentTween:Cancel() end
    currentTween = TweenService:Create(fillBar,
        TweenInfo.new(0.4, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
        Size = UDim2.new(fraction, 0, 1, 0),
        BackgroundColor3 = getHealthColor(fraction),
    })
    currentTween:Play()

    -- Trailing damage bar shrinks slower
    if damageTween then damageTween:Cancel() end
    damageTween = TweenService:Create(damageBar,
        TweenInfo.new(0.8, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
        Size = UDim2.new(fraction, 0, 1, 0),
    })
    task.delay(0.3, function()
        if damageTween then damageTween:Play() end
    end)
end

local function onCharacterAdded(character: Model)
    local humanoid = character:WaitForChild("Humanoid")
    updateHealthBar(humanoid.Health, humanoid.MaxHealth)
    humanoid.HealthChanged:Connect(function(newHealth: number)
        updateHealthBar(newHealth, humanoid.MaxHealth)
    end)
end

if player.Character then onCharacterAdded(player.Character) end
player.CharacterAdded:Connect(onCharacterAdded)

Notification Toast System

local TweenService = game:GetService("TweenService")
local Players = game:GetService("Players")

local toastContainer = Instance.new("Frame")
toastContainer.Size = UDim2.new(0.3, 0, 0.5, 0)
toastContainer.Position = UDim2.new(1, -16, 0, 16)
toastContainer.AnchorPoint = Vector2.new(1, 0)
toastContainer.BackgroundTransparency = 1
toastContainer.Parent = playerGui:WaitForChild("NotificationGui")

local listLayout = Instance.new("UIListLayout")
listLayout.FillDirection = Enum.FillDirection.Vertical
listLayout.VerticalAlignment = Enum.VerticalAlignment.Top
listLayout.Padding = UDim.new(0, 8)
listLayout.SortOrder = Enum.SortOrder.LayoutOrder
listLayout.Parent = toastContainer

local function showToast(message: string, duration: number?, color: Color3?)
    duration = duration or 3
    color = color or Color3.fromRGB(40, 40, 60)

    local toast = Instance.new("Frame")
    toast.Size = UDim2.new(1, 0, 0, 50)
    toast.BackgroundColor3 = color
    toast.BackgroundTransparency = 1
    toast.BorderSizePixel = 0
    toast.LayoutOrder = tick()
    toast.Parent = toastContainer

    Instance.new("UICorner", toast).CornerRadius = UDim.new(0, 8)

    local label = Instance.new("TextLabel")
    label.Size = UDim2.new(1, -24, 1, 0)
    label.Position = UDim2.new(0, 12, 0, 0)
    label.Text = message
    label.Font = Enum.Font.GothamBold
    label.TextSize = 14
    label.TextColor3 = Color3.fromRGB(255, 255, 255)
    label.TextTransparency = 1
    label.TextXAlignment = Enum.TextXAlignment.Left
    label.BackgroundTransparency = 1
    label.Parent = toast

    TweenService:Create(toast, TweenInfo.new(0.3, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
        BackgroundTransparency = 0.1,
    }):Play()
    TweenService:Create(label, TweenInfo.new(0.3), { TextTransparency = 0 }):Play()

    task.delay(duration, function()
        local fadeOut = TweenService:Create(toast, TweenInfo.new(0.3), { BackgroundTransparency = 1 })
        TweenService:Create(label, TweenInfo.new(0.3), { TextTransparency = 1 }):Play()
        fadeOut:Play()
        fadeOut.Completed:Connect(function() toast:Destroy() end)
    end)
end

-- Usage
showToast("Quest completed: Defeat 10 Goblins!")
showToast("Not enough coins!", 4, Color3.fromRGB(140, 30, 30))

UI Theme System

Define colors, fonts, and sizes as constants in a shared module. All LocalScripts reference it for visual consistency.
-- UITheme.luau (ModuleScript in ReplicatedStorage)
local Theme = {
    Colors = {
        Background   = Color3.fromRGB(25, 25, 35),
        Surface      = Color3.fromRGB(40, 40, 55),
        Primary      = Color3.fromRGB(0, 150, 70),
        Danger       = Color3.fromRGB(200, 40, 40),
        TextPrimary  = Color3.fromRGB(255, 255, 255),
        TextSecondary = Color3.fromRGB(180, 180, 190),
        Accent       = Color3.fromRGB(255, 215, 0),
    },
    Fonts = {
        Title = Enum.Font.GothamBold,
        Body  = Enum.Font.Gotham,
        Mono  = Enum.Font.RobotoMono,
    },
    TextSizes = {
        Title    = 24,
        Subtitle = 18,
        Body     = 14,
        Small    = 12,
    },
    CornerRadius = UDim.new(0, 8),
    Padding      = UDim.new(0, 12),
}

return Theme

GUI Pooling for Long Lists

For scrolling lists with many items (inventory, leaderboard), reuse GUI elements instead of creating and destroying them every update:
local pool: { Frame } = {}

local function getCard(): Frame
    local card = table.remove(pool)
    if not card then
        card = createNewCard() -- only create if pool is empty
    end
    card.Visible = true
    return card
end

local function returnCard(card: Frame)
    card.Visible = false
    card.Parent = nil
    table.insert(pool, card)
end

Common Mistakes

Never trust client UI for game logic. Always validate purchases, item grants, and state changes on the server. The client’s shop button fires a RemoteEvent; the server decides if the purchase succeeds.
-- BAD: breaks on different resolutions
frame.Position = UDim2.new(0, 500, 0, 300)
frame.Size = UDim2.new(0, 400, 0, 200)

-- GOOD: responsive
frame.Position = UDim2.new(0.5, 0, 0.5, 0)
frame.Size = UDim2.new(0.3, 0, 0.25, 0)
frame.AnchorPoint = Vector2.new(0.5, 0.5)
-- BAD: creates garbage every frame, causes lag
RunService.RenderStepped:Connect(function()
    local label = Instance.new("TextLabel")
    label.Text = "Score: " .. score
    label.Parent = screenGui
end)

-- GOOD: update existing element
RunService.RenderStepped:Connect(function()
    scoreLabel.Text = "Score: " .. score
end)
-- BAD: connection persists after UI is removed, leaks memory
button.Activated:Connect(function()
    doSomething()
end)

-- GOOD: store and disconnect, or use :Once() for single-fire
local connection = button.Activated:Connect(function()
    doSomething()
end)
-- later:
connection:Disconnect()
-- BAD: freezes the entire UI
button.Activated:Connect(function()
    task.wait(5)
    label.Text = "Done"
end)

-- GOOD: async work via task.spawn
button.Activated:Connect(function()
    button.Active = false
    task.spawn(function()
        task.wait(5)
        label.Text = "Done"
        button.Active = true
    end)
end)

Accessibility Checklist

  • Minimum 14pt body text; 12pt only for tertiary labels
  • High contrast between text and background
  • Visual feedback on all interactive elements (hover color, press scale)
  • Support GuiService:Select() for gamepad/console navigation
  • Use Activated instead of MouseButton1Click everywhere

Build docs developers (and LLMs) love