Building User Interfaces in Roblox
Introduction
Every game needs a way to communicate with the player. Health bars, inventory screens, score displays, menus, and buttons all fall under the category of User Interface (UI). In Roblox, building UI is a core skill that every game developer needs.
This tutorial walks you through the fundamentals of Roblox UI scripting. You will learn how the UI hierarchy works, how to position elements using UDim2, how to respond to player input with events, and how to animate your interfaces with TweenService. By the end, you will have built a complete interactive UI panel from scratch.
All examples in this tutorial assume you are writing a LocalScript placed inside StarterPlayerScripts or PlayerGui. LocalScripts run on the client, which is where player-specific UI lives.
The UI Hierarchy
Roblox UI is built from a tree of objects. At the root sits ScreenGui, which represents the entire UI layer rendered on the player’s screen. Every visible UI element is a descendant of a ScreenGui.
The hierarchy looks like this:
ScreenGui
├── Frame
│ ├── TextLabel
│ ├── TextButton
│ ├── ImageLabel
│ └── UIConstraint
├── TextBox
└── ScrollingFrame
Frame is a simple container that holds other elements. TextLabel displays read-only text, while TextButton is an interactive element players can click. ScrollingFrame provides a scrollable container for content that does not fit on screen.
All visual elements inherit from GuiObject, which provides the common properties you will use most: Size, Position, BackgroundColor3, and Visible.
UICorner is a decoration element that rounds the corners of any parent UI element. Set CornerRadius using UDim.new(0, radiusInPixels). For example, UDim.new(0, 8) creates 8-pixel radius corners. You will see UICorner used throughout this tutorial to give frames and buttons a softer, more modern appearance.
Understanding UDim2
Before placing any UI element, you need to understand UDim2. This is the data type Roblox uses for two-dimensional sizing and positioning. It can be confusing if you are coming from other frameworks.
An UDim2 has four components: scaleX, offsetX, scaleY, and offsetY.
UDim2.new(scaleX, offsetX, scaleY, offsetY)
The scale value is a fraction of the parent dimension. A scale of 0.5 means 50% of the parent’s width or height. The offset value is a fixed pixel amount. These two components are added together to produce the final size or position.
-- 100 pixels wide and 50 pixels tall (fixed size)
frame.Size = UDim2.new(0, 100, 0, 50)
-- Half the parent's width, 100 pixels tall
frame.Size = UDim2.new(0.5, 0, 0, 100)
-- Full width minus 20 pixels, full height minus 40 pixels
frame.Size = UDim2.new(1, -20, 1, -40)
The same UDim2 structure works for both Size and Position, so track which property you are setting.
AnchorPoint and True Centering
A common mistake is trying to center an element by setting its Position to (0.5, 0, 0.5, 0) without also setting AnchorPoint. By default, AnchorPoint is {0, 0}, which means the element’s top-left corner aligns to its Position. This puts the top-left corner at the screen center instead of centering the element itself.
Set AnchorPoint to {0.5, 0.5} so the element’s center aligns with its Position value:
frame.AnchorPoint = Vector2.new(0.5, 0.5) -- Center point as origin
frame.Position = UDim2.new(0.5, 0, 0.5, 0) -- Center of screen
Now the element is truly centered on screen. The same logic applies when centering an element inside a container: set the container’s AnchorPoint to center, then set its Position to center the container itself.
Creating Your First ScreenGui
Start by creating the container and a simple frame. Place this inside a LocalScript:
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
-- Create a ScreenGui
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "MyFirstGui"
screenGui.ResetOnSpawn = false -- UI stays after player respawns
screenGui.Parent = playerGui
-- Create a frame
local panel = Instance.new("Frame")
panel.Name = "MainPanel"
panel.Size = UDim2.new(0, 320, 0, 200)
panel.AnchorPoint = Vector2.new(0.5, 0.5)
panel.Position = UDim2.new(0.5, 0, 0.5, 0)
panel.BackgroundColor3 = Color3.fromRGB(30, 30, 45)
panel.BorderSizePixel = 0
panel.Parent = screenGui
The ResetOnSpawn = false flag is important for games where players respawn frequently. Without it, the UI disappears each time the character dies.
The use of WaitForChild on the PlayerGui reference is a safety pattern that ensures the player’s GUI has loaded before you try to parent anything to it. Direct access like player.PlayerGui can return nil if the GUI has not finished loading.
Adding Text and Buttons
Add a title label and a clickable button inside the panel:
-- Title label
local title = Instance.new("TextLabel")
title.Name = "Title"
title.Size = UDim2.new(1, 0, 0, 40)
title.Position = UDim2.new(0, 0, 0, 10)
title.BackgroundTransparency = 1
title.Text = "Welcome Panel"
title.TextColor3 = Color3.fromRGB(255, 255, 255)
title.TextSize = 22
title.Font = Enum.Font.GothamBold
title.Parent = panel
-- Clickable button
local actionButton = Instance.new("TextButton")
actionButton.Name = "ActionButton"
actionButton.Size = UDim2.new(0, 140, 0, 44)
actionButton.Position = UDim2.new(0.5, 0, 0, 70)
actionButton.AnchorPoint = Vector2.new(0.5, 0)
actionButton.BackgroundColor3 = Color3.fromRGB(70, 130, 220)
actionButton.TextColor3 = Color3.fromRGB(255, 255, 255)
actionButton.Text = "Click Me"
actionButton.TextSize = 18
actionButton.Font = Enum.Font.GothamBold
actionButton.AutoButtonColor = false -- We handle hover ourselves
actionButton.Parent = panel
TextButton inherits from GuiButton, which means it has built-in click events. Connect to MouseButton1Click to respond to left clicks:
actionButton.MouseButton1Click:Connect(function()
print("Button was clicked")
end)
The output when the button is clicked is: Button was clicked
Hover Effects with MouseEnter and MouseLeave
A polished UI gives visual feedback when the mouse enters or leaves an interactive element. Roblox provides MouseEnter and MouseLeave events for this purpose.
actionButton.MouseEnter:Connect(function()
-- Lighten the button when the cursor is over it
actionButton.BackgroundColor3 = Color3.fromRGB(90, 160, 255)
end)
actionButton.MouseLeave:Connect(function()
-- Restore the original color when the cursor leaves
actionButton.BackgroundColor3 = Color3.fromRGB(70, 130, 220)
end)
The output when the mouse enters the button is the color change. When the mouse leaves, the color reverts.
Animating UI with TweenService
Instant state changes feel abrupt. TweenService lets you smoothly transition between property values over time. To use it, create a TweenInfo object that describes the animation, then call TweenService:Create to build the tween.
local TweenService = game:GetService("TweenService")
local hoverInfo = TweenInfo.new(
0.2, -- Duration in seconds
Enum.EasingStyle.Quad, -- Smooth acceleration curve
Enum.EasingDirection.Out -- Ease out for natural feel
)
Now connect hover events with tween animations instead of instant color changes:
actionButton.MouseEnter:Connect(function()
local tween = TweenService:Create(actionButton, hoverInfo, {
BackgroundColor3 = Color3.fromRGB(90, 160, 255),
Size = UDim2.new(0, 150, 0, 48) -- Grow slightly
})
tween:Play()
end)
actionButton.MouseLeave:Connect(function()
local tween = TweenService:Create(actionButton, hoverInfo, {
BackgroundColor3 = Color3.fromRGB(70, 130, 220),
Size = UDim2.new(0, 140, 0, 44) -- Shrink back
})
tween:Play()
end)
The button now smoothly grows and changes color when hovered. Tweens are non-blocking, so multiple elements can animate at the same time without interfering with each other.
Making a Health Bar
Health bars are one of the most common UI elements in games. A health bar typically shows the current health as a filled portion of a background container. The key technique is sizing the fill frame as a proportion of the total width.
-- Background (empty bar)
local healthBarBg = Instance.new("Frame")
healthBarBg.Name = "HealthBarBg"
healthBarBg.Size = UDim2.new(0, 260, 0, 24)
healthBarBg.Position = UDim2.new(0.5, 0, 0, 120)
healthBarBg.AnchorPoint = Vector2.new(0.5, 0)
healthBarBg.BackgroundColor3 = Color3.fromRGB(40, 40, 50)
healthBarBg.BorderSizePixel = 0
healthBarBg.Parent = panel
-- Rounded corners via UICorner
local bgCorner = Instance.new("UICorner")
bgCorner.CornerRadius = UDim.new(0, 6)
bgCorner.Parent = healthBarBg
-- Foreground (filled portion)
local healthFill = Instance.new("Frame")
healthFill.Name = "HealthFill"
healthFill.Size = UDim2.new(1, 0, 1, 0) -- Start at full
healthFill.BackgroundColor3 = Color3.fromRGB(80, 200, 80)
healthFill.BorderSizePixel = 0
healthFill.Parent = healthBarBg
-- Rounded corners for the fill
local fillCorner = Instance.new("UICorner")
fillCorner.CornerRadius = UDim.new(0, 6)
fillCorner.Parent = healthFill
The fillCorner matches bgCorner so the fill does not overlap the rounded corners of the background. To add a visible border instead of relying on the dark background contrast, you would use UIStroke and set its Color and Thickness properties.
Now create a function that updates the fill based on current and maximum health:
local function updateHealthBar(currentHealth, maxHealth)
local ratio = math.clamp(currentHealth / maxHealth, 0, 1)
healthFill.Size = UDim2.new(ratio, 0, 1, 0)
-- Change color based on health level
if ratio > 0.6 then
healthFill.BackgroundColor3 = Color3.fromRGB(80, 200, 80) -- Green
elseif ratio > 0.3 then
healthFill.BackgroundColor3 = Color3.fromRGB(220, 200, 60) -- Yellow
else
healthFill.BackgroundColor3 = Color3.fromRGB(220, 60, 60) -- Red
end
end
-- Test the function with different values
updateHealthBar(100, 100)
task.wait(2)
updateHealthBar(65, 100)
task.wait(2)
updateHealthBar(25, 100)
The fill width updates as a fraction of the total bar width, and the color shifts from green to yellow to red as health decreases.
Scrolling Content with ScrollingFrame
When you have more content than fits on screen, ScrollingFrame provides scrollable real estate. It works like a Frame but adds scroll bars.
-- Scrollable container
local scrollFrame = Instance.new("ScrollingFrame")
scrollFrame.Name = "ScrollContent"
scrollFrame.Size = UDim2.new(0, 280, 0, 140)
scrollFrame.Position = UDim2.new(0.5, 0, 0, 155)
scrollFrame.AnchorPoint = Vector2.new(0.5, 0)
scrollFrame.BackgroundColor3 = Color3.fromRGB(20, 20, 30)
scrollFrame.BorderSizePixel = 0
scrollFrame.ScrollBarThickness = 6
scrollFrame.ScrollingDirection = Enum.ScrollingDirection.Y
scrollFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
scrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
scrollFrame.Parent = panel
local scrollCorner = Instance.new("UICorner")
scrollCorner.CornerRadius = UDim.new(0, 6)
scrollCorner.Parent = scrollFrame
-- Add some text items to the scroll frame
for i = 1, 8 do
local itemLabel = Instance.new("TextLabel")
itemLabel.Name = "Item_" .. i
itemLabel.Size = UDim2.new(1, -16, 0, 30)
itemLabel.Position = UDim2.new(0, 8, 0, (i - 1) * 36)
itemLabel.BackgroundTransparency = 1
itemLabel.Text = "Inventory Item " .. i
itemLabel.TextColor3 = Color3.fromRGB(200, 200, 220)
itemLabel.TextSize = 14
itemLabel.Font = Enum.Font.Gotham
itemLabel.TextXAlignment = Enum.TextXAlignment.Left
itemLabel.Parent = scrollFrame
end
AutomaticCanvasSize = Enum.AutomaticSize.Y makes the canvas grow automatically as you add content, eliminating the need to manually resize it.
Layout Containers with UIListLayout
Manually positioning every element becomes tedious as your UI grows. Layout containers like UIListLayout automatically arrange children, handling spacing and alignment for you.
local listLayout = Instance.new("UIListLayout")
listLayout.SortOrder = Enum.SortOrder.LayoutOrder
listLayout.Padding = UDim.new(0, 8) -- 8 pixels between items
listLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
listLayout.VerticalAlignment = Enum.VerticalAlignment.Top
listLayout.Parent = panel
With this layout in place, any child element with a LayoutOrder property set will be automatically positioned in sequence. This is much cleaner than calculating each Position manually.
Common Mistakes to Avoid
A few patterns trip up developers who are new to Roblox UI scripting.
Confusing scale and offset in UDim2. UDim2.new(0, 100, 0, 50) gives 100 pixels by 50 pixels, not 100%. The scale is the first argument, offset is the second. Mixing these up leads to UI elements that are either tiny or fill the entire screen unexpectedly.
Forgetting AnchorPoint when centering. Without AnchorPoint = Vector2.new(0.5, 0.5), a Position of (0.5, 0, 0.5, 0) places the element’s top-left corner at the center of the screen instead of centering the element itself.
Using LocalPlayer in a server Script. game.Players.LocalPlayer is only available in LocalScripts. In server Scripts, it returns nil. If you need to update a player’s UI from the server, use a RemoteEvent to tell the client to make the change.
Not waiting for GUI to load. Directly accessing player.PlayerGui.ScreenGui can return nil if Roblox has not finished loading the player’s interface. Use WaitForChild instead of direct access to ensure the element exists before parenting to it.
Missing ClipsDescendants on containers. When a child element extends beyond its parent’s bounds, it still renders on top unless you enable clipping. Set container.ClipsDescendants = true to cut off any overflowing content. This matters most for frames that hold smaller elements, like the health bar background in the example above. Without clipping, the fill frame would render outside the rounded corners of its parent.
Summary
You now have a working foundation in Roblox UI scripting. The key concepts to remember are:
ScreenGuiis the root container for all player UIUDim2combines scale (fraction of parent) and offset (fixed pixels) for sizing and positioningAnchorPointcontrols which part of an element aligns with itsPositionUICorneradds rounded corners to any UI element via itsCornerRadiuspropertyGuiObjectevents likeMouseButton1Click,MouseEnter, andMouseLeavelet you respond to player inputTweenServicecreates smooth animations between property valuesScrollingFrameandUIListLayouthandle overflow content and automatic arrangementUIStrokeadds visible outlines and borders to UI elements
From here, you can explore more advanced topics like ViewportFrame for 3D previews, UIScale for responsive scaling, UIStroke for custom borders, and RemoteEvents for communicating between client and server. Each of these builds directly on the patterns you learned in this tutorial.
Practice by rebuilding the examples with different colors, sizes, and animations. Try adding a second panel that opens when you click the button, or a score display that updates when something happens in the game world. The best way to learn is to build.
See Also
- Roblox Lua Getting Started — Set up Roblox Studio and write your first Lua script
- Roblox Scripting Objects — How instances, parenting, and properties work in Roblox
- Lua Metatables — The metatable system that powers Roblox objects and OOP patterns