원클릭으로
roblox-gui
GUI systems, layout, responsiveness, cross-platform UI. ScreenGuis, UIListLayout, constraint-based design.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
GUI systems, layout, responsiveness, cross-platform UI. ScreenGuis, UIListLayout, constraint-based design.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
DataStores, ProfileStore, session locking, data persistence patterns.
Animations, particles, tweens, ContentProvider, visual effects.
Luau language fundamentals, type system, OOP, deprecation table, error patterns.
ProcessReceipt correctness, prompt APIs, purchase reconciliation, session-lock interaction.
Server-authoritative networking, RemoteEvent validation, rate limiting, exploit prevention, security hardening.
StreamingEnabled, performance optimization, memory management, object pooling, mobile targets.
| name | roblox-gui |
| description | GUI systems, layout, responsiveness, cross-platform UI. ScreenGuis, UIListLayout, constraint-based design. |
| last_reviewed | "2026-05-22T00:00:00.000Z" |
Load this reference when working on any UI-related task in Roblox:
All GUI code runs on the client (LocalScripts). UI objects live under StarterGui at edit time and are cloned into each player's PlayerGui at runtime.
Never place UI elements directly under a ScreenGui. Always create a transparent Container Frame as the first child with Size = {1,0},{1,0} and BackgroundTransparency = 1. Its AbsoluteSize always matches screen resolution.
local container = Instance.new("Frame")
container.Name = "Container"
container.Size = UDim2.new(1, 0, 1, 0)
container.BackgroundTransparency = 1
container.BorderSizePixel = 0
container.Parent = screenGui
-- All UI children go under container, not directly under screenGui
UDim.new(0.5, 0) = 50% radius).-- Scale for responsive sizing
frame.Size = UDim2.new(0.5, 0, 0, 40) -- 50% width, 40px height
-- Offset for UIStroke (Scale not supported)
stroke.Thickness = 2 -- always Offset
-- UICorner supports Scale
corner.CornerRadius = UDim.new(0, 8) -- Offset
corner.CornerRadius = UDim.new(0.5, 0) -- Scale (50% of smallest axis)
ScreenGui.ScreenInsets.#FFFFFF) on pure black (#000000). Use off-white (#F0F0F0) on dark gray (#1E1E1E).20,20,20 (Modern Black) to 35,35,35 (Very Light). Don't go lighter than 35.| Mistake | Fix |
|---|---|
| Elements directly under ScreenGui | Use a Container Frame child |
| Pure Scale only | Too small on mobile — add Offset minimums |
| Pure Offset only | Breaks on different resolutions |
| Pure white on pure black text | Use off-white on dark gray |
| Ignoring mobile players | 50%+ are mobile; design touch-first |
| Text-heavy UI for young audiences | Use icons, images, minimal text |
| UI overlapping top bar / chat / leaderboard | Respect safe areas (top 58px, bottom 100px) |
| Not testing with Device Emulator | Always test before publishing |
The primary container for all 2D UI. 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
Key Properties:
| Property | Purpose |
|---|---|
DisplayOrder | Controls layering. Higher values render on top of lower values. |
ResetOnSpawn | true (default): destroyed and re-cloned on respawn. Set to false for persistent UI (shops, settings). |
IgnoreGuiInset | true: UI extends behind the top bar (CoreGui area). Use for fullscreen overlays. |
Enabled | Toggle visibility without destroying. |
Renders UI on a Part's surface. Used for in-world signs, screens, 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
Always faces the camera. Used for nametags, damage numbers, 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
DisplayOrder 100 -- Modal dialogs (on top of everything)
DisplayOrder 50 -- Notifications / toasts
DisplayOrder 10 -- HUD elements
DisplayOrder 1 -- Background UI
Container for grouping and styling. No text or image by default.
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 (with AnchorPoint)
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
local label = Instance.new("TextLabel")
label.Size = UDim2.new(1, 0, 0, 40)
label.Text = "Score: 0"
label.TextColor3 = Color3.fromRGB(255, 255, 255)
label.TextScaled = true
label.Font = Enum.Font.GothamBold
label.BackgroundTransparency = 1
label.Parent = frame
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)
Use
Activatedinstead ofMouseButton1Clickfor cross-platform support.
local icon = Instance.new("ImageLabel")
icon.Size = UDim2.new(0, 64, 0, 64)
icon.Image = "rbxassetid://123456789"
icon.ScaleType = Enum.ScaleType.Fit
icon.BackgroundTransparency = 1
icon.Parent = frame
See section 14 (ScrollingFrame Patterns) for full coverage including AutomaticCanvasSize, UIListLayout integration, and elastic overscroll.
Renders 3D content inside a 2D GUI (item previews, character displays).
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
| Modifier | Purpose |
|---|---|
UIListLayout | Arranges children in a vertical or horizontal list |
UIGridLayout | Arranges children in a grid |
UIPageLayout | Swipeable pages (one child visible at a time) |
UIPadding | Inner padding on a container |
UICorner | Rounded corners |
UIStroke | Outline/border effect |
UIGradient | Color gradient on an element |
UISizeConstraint | Min/max pixel size |
UIAspectRatioConstraint | Locks width/height ratio |
-- Rounded corners
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 8)
corner.Parent = frame
-- Stroke/border
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
-- Gradient
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
-- Padding
local padding = Instance.new("UIPadding")
padding.PaddingLeft = UDim.new(0, 12)
padding.PaddingRight = UDim.new(0, 12)
padding.PaddingTop = UDim.new(0, 12)
padding.PaddingBottom = UDim.new(0, 12)
padding.Parent = frame
Arranges children sequentially. Best for menus, sidebars, chat messages, vertical/horizontal lists.
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).
Arranges children in rows and columns. Best for inventories, shops, icon grids.
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
Shows one child at a time with animated page transitions. Best for tabbed menus, tutorials, onboarding flows.
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 -- cannot loop from last to first
pageLayout.Padding = UDim.new(0, 0)
pageLayout.Parent = frame
-- Navigate pages
pageLayout:JumpToIndex(0)
pageLayout:Next()
pageLayout:Previous()
pageLayout:JumpTo(someChildFrame)
| Scenario | Layout |
|---|---|
| Vertical menu, chat log, leaderboard | UIListLayout |
| Inventory grid, shop items, emoji picker | UIGridLayout |
| Settings tabs, tutorial slides, shop categories | UIPageLayout |
| HUD element pinned to a corner | Absolute positioning (no layout) |
| Overlapping elements (health bar segments) | Absolute positioning |
Position and Size directly. Use for HUD elements pinned to specific screen locations.Position. Use for dynamic lists where content count changes.You can nest both: a frame with absolute position containing children managed by a UIListLayout.
Scale vs Offset rules are in Design Guidelines above. This section covers constraints and adaptive patterns.
Prevents elements from becoming too small on phones or too large on ultrawide monitors.
local sizeConstraint = Instance.new("UISizeConstraint")
sizeConstraint.MinSize = Vector2.new(200, 150)
sizeConstraint.MaxSize = Vector2.new(600, 450)
sizeConstraint.Parent = frame
Locks an element's aspect ratio so it does not stretch.
local aspect = Instance.new("UIAspectRatioConstraint")
aspect.AspectRatio = 16 / 9
aspect.AspectType = Enum.AspectType.FitWithinMaxSize
aspect.DominantAxis = Enum.DominantAxis.Width
aspect.Parent = frame
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()
local TweenService = game:GetService("TweenService")
-- TweenInfo.new(time, easingStyle, easingDirection, repeatCount, reverses, delayTime)
local tweenInfo = TweenInfo.new(
0.5, -- duration in seconds
Enum.EasingStyle.Quad, -- easing curve
Enum.EasingDirection.Out, -- direction
0, -- repeat count (0 = no repeat, -1 = infinite)
false, -- reverses
0 -- delay before starting
)
Common Easing Styles:
| Style | Use Case |
|---|---|
Quad | General-purpose, smooth |
Back | Slight overshoot, bouncy buttons |
Elastic | Springy, attention-grabbing |
Bounce | Bouncing effect at the end |
Linear | Constant speed, progress bars |
Sine | Gentle, subtle motion |
Exponential | Dramatic acceleration/deceleration |
-- 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()
-- Fade in
frame.BackgroundTransparency = 1
local fadeIn = TweenService:Create(frame, TweenInfo.new(0.3), {
BackgroundTransparency = 0,
})
fadeIn:Play()
-- Color transition
local colorShift = TweenService:Create(frame, TweenInfo.new(0.5), {
BackgroundColor3 = Color3.fromRGB(255, 50, 50),
})
colorShift:Play()
-- Size pulse
local pulse = TweenService:Create(button, TweenInfo.new(0.6, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, true), {
Size = UDim2.new(0.55, 0, 0, 55),
})
pulse:Play()
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()
-- Bounce entrance
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
Low-level input detection. Best for keyboard shortcuts, mouse tracking, detecting input type.
local UserInputService = game:GetService("UserInputService")
-- Keyboard input
UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessed: boolean)
if gameProcessed then return end -- ignore if typing in a TextBox, etc.
if input.KeyCode == Enum.KeyCode.E then
toggleInventory()
elseif input.KeyCode == Enum.KeyCode.Escape then
togglePauseMenu()
end
end)
-- Mouse position
local mousePos = UserInputService:GetMouseLocation() -- Vector2
-- Detect platform
local isMobile = UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
local isConsole = UserInputService.GamepadEnabled
-- Hide mouse cursor
UserInputService.MouseIconEnabled = false
Higher-level action binding. Automatically generates mobile buttons. Best for game actions (interact, reload, sprint).
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 -- consume the input
end
-- Bind to E key, touch button auto-created on mobile
ContextActionService:BindAction("Interact", onInteract, true, Enum.KeyCode.E)
-- Customize the mobile button
ContextActionService:SetPosition("Interact", UDim2.new(0.8, 0, 0.5, 0))
ContextActionService:SetTitle("Interact", "E")
ContextActionService:SetImage("Interact", "rbxassetid://123456789")
-- Unbind when no longer needed
ContextActionService:UnbindAction("Interact")
| Service | Best For |
|---|---|
UserInputService | Global hotkeys, mouse tracking, detecting input device type, custom cursor |
ContextActionService | In-game actions that need mobile buttons, context-sensitive controls (e.g., "interact" only near objects) |
GuiButton.Activated | UI button clicks (already cross-platform) |
Problem: Display purchasable items in a grid with hover feedback and server-validated purchase.
Structure:
Key properties: UIGridLayout.CellSize for responsive cards, UICorner for rounded edges, TweenService for hover color shift. Purchase via RemoteFunction (server validates, returns success/fail).
Problem: Show player health with color thresholds and damage trail effect.
Structure:
Key logic: On health change, tween fill Size.X.Scale to health/maxHealth. Color lerp between green/yellow/red at thresholds (0.6, 0.3). Damage trail: delay 0.3s then tween to match fill.
Problem: Temporary message that slides in, stays briefly, slides out.
Structure:
Key logic: Tween Position from off-screen to visible, task.delay(3), tween back out, Destroy. Queue multiple toasts with vertical offset.
Problem: Modal dialog with backdrop, title, body, confirm/cancel buttons.
Structure:
Key logic: Show/hide by toggling Visible + tween BackgroundTransparency on backdrop. Return result via Promise or callback. Destroy on close.
Position and Size to support all screen resolutions.AnchorPoint = Vector2.new(0.5, 0.5).-- 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
TextScaled = true with TextWrapped = true sparingly; prefer explicit TextSize for control.GuiService:Select() for console players.For scrolling lists with many items (inventory, leaderboard), reuse GUI elements instead of creating/destroying them.
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
ScreenGui.Enabled = false when a UI is not visible rather than destroying and recreating it.Activated on buttons instead of MouseButton1Click for cross-platform support.-- 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
local connection = button.Activated:Connect(function()
doSomething()
end)
-- When done:
connection:Disconnect()
-- OR use :Once() for single-fire events
button.Activated:Once(function()
doSomething()
end)
-- BAD: freezes the entire UI
button.Activated:Connect(function()
task.wait(5) -- nothing else can happen for 5 seconds
label.Text = "Done"
end)
-- GOOD: use task.delay or task.spawn for async work
button.Activated:Connect(function()
button.Active = false
task.spawn(function()
-- do async work
task.wait(5)
label.Text = "Done"
button.Active = true
end)
end)
-- BAD: client decides if purchase succeeds
button.Activated:Connect(function()
coins -= item.price -- client-side deduction, exploitable
end)
-- GOOD: server validates everything
button.Activated:Connect(function()
local success = purchaseRemote:InvokeServer(itemId)
if success then
updateCoinsDisplay()
end
end)
-- BAD: only works with mouse
button.MouseButton1Click:Connect(handler)
-- GOOD: works on mouse, touch, and gamepad
button.Activated:Connect(handler)
78% of Roblox traffic is mobile. Design for touch first, adapt for desktop/gamepad.
local UserInputService = game:GetService("UserInputService")
local function isTouchDevice(): boolean
return UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
end
-- Apply at UI creation time
if isTouchDevice() then
button.Size = UDim2.new(button.Size.X.Scale * 1.4, 0, button.Size.Y.Scale * 1.4, 0)
end
Modern approach to input detection. Use instead of checking individual booleans:
local preferred = UserInputService.PreferredInput
-- Enum.PreferredInput values:
-- Touch, MouseAndKeyboard, Gamepad, None
if preferred == Enum.PreferredInput.Touch then
-- enlarge buttons, simplify layout
elseif preferred == Enum.PreferredInput.Gamepad then
-- highlight focused element, add navigation hints
end
UserInputService:GetPropertyChangedSignal("PreferredInput"):Connect(function()
-- re-adapt UI when input method changes
end)
ScreenGui.IgnoreGuiInset = true for fullscreen, but don't put critical content behind it.UIScale to zoom entire UI proportionally on small screens:local uiScale = Instance.new("UIScale")
uiScale.Scale = math.min(1, camera.ViewportSize.X / 1080) -- reference width
uiScale.Parent = screenGui
For console players:
local GuiService = game:GetService("GuiService")
-- Set the initially selected object when opening a menu
GuiService.SelectedObject = myButton
-- On menu close, clear selection
GuiService.SelectedObject = nil
Source: Roblox Adaptive Design Guidelines (create.roblox.com/docs/production/publishing/adaptive-design)
Do NOT use TextScaled = true. It makes text unreadably small on mobile and truncates text that doesn't fit.
Use AutomaticSize instead. It resizes the UI element to fit the text at a consistent, readable font size:
-- BAD: text scales to fit, becomes unreadable on small screens
label.TextScaled = true
-- GOOD: label grows/shrinks to fit text at fixed readable size
label.TextSize = 16
label.TextWrapped = true
label.AutomaticSize = Enum.AutomaticSize.Y -- height adjusts to content
If you must use TextScaled, always add a constraint:
local textSizeConstraint = Instance.new("UITextSizeConstraint")
textSizeConstraint.MaxTextSize = 24
textSizeConstraint.MinTextSize = 12 -- NEVER below 9 (official hard rule)
textSizeConstraint.Parent = label
label.TextTruncate = Enum.TextTruncate.AtEnd -- shows "..." when text overflows
label.TextWrapped = true -- wrap instead of truncate for multi-line content
local TextService = game:GetService("TextService")
local bounds = TextService:GetTextSize(
"Hello World",
16, -- TextSize
Enum.Font.Gotham,
Vector2.new(200, math.huge) -- max width, unlimited height
)
-- bounds.X = actual width, bounds.Y = actual height
Source: Roblox Size Modifiers & Constraints docs (create.roblox.com/docs/ui/size-modifiers)
Roblox ships a CSS-like styling system (2025+). Use it for consistent theming.
Named values (like CSS variables). Define once, reference everywhere.
-- Style rules can target by class, tag, name, state, modifier, query
-- Similar to CSS selectors
| Approach | Best For |
|---|---|
| Studio StyleEditor | Static UI built visually, team collaboration on design |
| Code-based UITheme module | Dynamic UI built in code, programmatic theming |
Both can coexist. StyleEditor is the "build in Studio" answer; code themes are the "build in code" answer.
Source: Roblox UI Styling docs (create.roblox.com/docs/ui/styling)
-- BAD: manually calculating canvas size
scrollFrame.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y)
-- GOOD: engine handles it automatically
scrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
scrollFrame.ScrollingDirection = Enum.ScrollingDirection.Y
The most common pattern — a scrollable list:
local scrollFrame = Instance.new("ScrollingFrame")
scrollFrame.Size = UDim2.new(1, 0, 1, 0)
scrollFrame.BackgroundTransparency = 1
scrollFrame.ScrollBarThickness = 6
scrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
scrollFrame.ScrollingDirection = Enum.ScrollingDirection.Y
scrollFrame.Parent = container
local listLayout = Instance.new("UIListLayout")
listLayout.FillDirection = Enum.FillDirection.Vertical
listLayout.Padding = UDim.new(0, 8)
listLayout.SortOrder = Enum.SortOrder.LayoutOrder
listLayout.Parent = scrollFrame
-- Add items as children of scrollFrame
-- They auto-arrange vertically, scrollFrame auto-sizes
For scrollable grids (inventory, shop):
local scrollFrame = Instance.new("ScrollingFrame")
scrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
scrollFrame.ScrollingDirection = Enum.ScrollingDirection.Y
scrollFrame.Parent = container
local gridLayout = Instance.new("UIGridLayout")
gridLayout.CellSize = UDim2.new(0, 80, 0, 80)
gridLayout.CellPadding = UDim2.new(0, 8, 0, 8)
gridLayout.FillDirectionMaxCells = 4 -- 4 columns
gridLayout.SortOrder = Enum.SortOrder.LayoutOrder
gridLayout.Parent = scrollFrame
The bouncy effect on iOS/Android. Enabled by default. Disable if unwanted:
scrollFrame.ElasticBehavior = Enum.ElasticBehavior.Never
Source: Roblox Scrolling Frames docs (create.roblox.com/docs/ui/scrolling-frames)
For complex UI with dynamic state, consider a reactive framework instead of manual Instance manipulation.
The gold standard for reactive Roblox UI. Declarative syntax with state management:
local Fusion = require(ReplicatedStorage.Fusion)
local New, Value, Computed, Children = Fusion.New, Fusion.Value, Fusion.Computed, Fusion.Children
local count = Value(0)
local counterGui = New("ScreenGui")({
[Children] = New("TextLabel")({
Text = Computed(function()
return `Count: ${count:get()}`
end),
Size = UDim2.new(0, 200, 0, 50),
})
})
-- Update state, UI re-renders automatically
count:set(count:get() + 1)
When the state changes, only the affected properties re-render. No manual updates needed.
Solid.js-inspired. Lighter weight, more concise:
local vide = require(ReplicatedStorage.vide)
local create, source, effect = vide.create, vide.source, vide.effect
local count = source(0)
local gui = create("ScreenGui")({
create("TextLabel")({
Text = function() return `Count: ${count()}` end,
Size = UDim2.new(0, 200, 0, 50),
})
})
| Scenario | Approach |
|---|---|
| Simple HUD (health bar, ammo) | Manual Instance manipulation. No framework needed. |
| Shop with filtering/sorting | Fusion or Vide. State changes drive UI updates. |
| Settings menu with toggles | Either works. Manual if <5 toggles, reactive if more. |
| Complex inventory with drag-and-drop | Fusion. State management pays off at this complexity. |
If the UI is simple (a few labels, a button, a health bar), manual Instance manipulation is fine. Don't pull in Fusion for a HUD. The framework pays for itself when state changes are frequent and UI is complex.