Foundations
- Put UI in
StarterGuiasScreenGuis (copied to each player'sPlayerGui), or create it from a client controller. UI code is client-only; the server never touchesPlayerGuiin normal designs. ScreenGuidefaults to set:ResetOnSpawn = false(keep UI across deaths),ZIndexBehavior = Sibling,IgnoreGuiInsetper design,ScreenInsets = CoreUISafeInsetsfor interactive UI (clears notches and the Roblox top bar);DeviceSafeInsets/Noneonly for full-bleed backgrounds.- Size and position with Scale (
UDim2.fromScale) plus constraints; use Offset only for fixed details (padding, strokes, icons). Pure-Offset UI breaks on phones and 4K screens. AnchorPoint = Vector2.new(0.5, 0.5)+Position = UDim2.fromScale(0.5, 0.5)centers an element.UIAspectRatioConstraintkeeps shapes square/consistent;UISizeConstraintcaps sizes;UITextSizeConstraintwithTextScaledbounds text size (prefer fixedTextSize+AutomaticSizewhere possible —TextScaledproduces inconsistent sizes).AutomaticSizelets frames grow with content (lists, tooltips, chat bubbles).- Test every screen in the Device Emulator (phone portrait/landscape, tablet, console 10-foot, 4K).
Layout
| Need | Use |
|---|---|
| Rows/columns that adapt | UIListLayout (FillDirection, Padding, HorizontalFlex/VerticalFlex, Wraps) |
| One item grows to fill space | UIFlexItem child with FlexMode = Fill (or Grow/Shrink/Custom) |
| Grids (inventory) | UIGridLayout inside a ScrollingFrame with AutomaticCanvasSize |
| Carousels/pages | UIPageLayout |
| Rounded corners, borders, padding | UICorner, UIStroke, UIPadding |
| Gradients, shadows | UIGradient, UIShadow |
| Scale a whole group | UIScale |
--!strict
local Players = game:GetService("Players")
local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "Hud"
screenGui.ResetOnSpawn = false
screenGui.ScreenInsets = Enum.ScreenInsets.CoreUISafeInsets
local bar = Instance.new("Frame")
bar.Name = "Toolbar"
bar.AnchorPoint = Vector2.new(0.5, 1)
bar.Position = UDim2.new(0.5, 0, 1, -16)
bar.Size = UDim2.fromScale(0.6, 0.1)
bar.BackgroundTransparency = 0.3
bar.Parent = screenGui
local list = Instance.new("UIListLayout")
list.FillDirection = Enum.FillDirection.Horizontal
list.HorizontalFlex = Enum.UIFlexAlignment.Fill -- slots share the width equally
list.Padding = UDim.new(0, 8)
list.Parent = bar
Instance.new("UICorner").Parent = bar
local padding = Instance.new("UIPadding")
padding.PaddingLeft, padding.PaddingRight = UDim.new(0, 8), UDim.new(0, 8)
padding.Parent = bar
for slot = 1, 5 do
local button = Instance.new("TextButton")
button.Name = `Slot{slot}`
button.Text = tostring(slot)
button.Size = UDim2.fromScale(0, 1)
button.Parent = bar
Instance.new("UIAspectRatioConstraint").Parent = button
end
screenGui.Parent = playerGui -- parent last: one layout pass, no flicker
Styling: StyleSheets (CSS-like)
Roblox's engine-level styling replaces copy-pasted property values:
StyleSheetwithStyleRules (Selectorlike"TextButton",".Primary"(tag),"#Close"(name),"TextButton:Hover"(state),"::UICorner"(modifier pseudo-instance)) andrule:SetProperties({...}).- Tokens are attributes on a token sheet (
$Primary,$Radius); themes are sheets deriving tokens (StyleDerive); swap themes by retargeting theStyleDerive. - A
StyleLinkunder aScreenGuiapplies one sheet to that tree. Style queries (@Name) switch styles by screen size, input type, or accessibility settings. - Edit visually in Studio's Style Editor. Details and a code example: references/styling-and-frameworks.md.
Cross-platform input in UI
- Read
UserInputService.PreferredInput(KeyboardAndMouse,Touch,Gamepad) and listen toUserInputService:GetPropertyChangedSignal("PreferredInput")to swap button prompts and layouts. Don't infer platform fromTouchEnabled/KeyboardEnabledalone. - Use
GuiButton.Activated(works for mouse, touch, and gamepad) instead ofMouseButton1Click. - Gamepad/console: every interactive element
Selectable, setGuiService.SelectedObjectwhen a menu opens, group withSelectionGroup/NextSelection*for predictable D-pad navigation, supportButtonBto close. Minimum touch target ≈ 44×44 px; readable text ≥ 14 px on phones. - Bind actions through the Input Action System (
InputBinding.UIButtonmakes an on-screen button trigger an action) — seeroblox-input.
Text
RichText = trueenables<b>,<i>,<font color="#FF0">,<stroke>, etc. Escape user text.- Any text from one player shown to another must be filtered with
TextService:FilterStringAsyncon the server (roblox-text-chat). That includes pet names, signs, guild names, trade notes. - Use
FontFace = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Bold)for weights. - Localize with
LocalizationTables and auto-translation; avoid baking text into images.
Animation and feel
- Tween on the client:
TweenService:Create(frame, TweenInfo.new(0.2, Enum.EasingStyle.Quad), { Position = ... }):Play(). - Animate
UIScale.Scalefor pop effects rather thanSize(keeps layout stable). CanvasGroupfades a whole subtree (GroupTransparency) but renders to a texture: use it for fading panels, not for large always-visible UI (memory/quality cost).- Drag-and-drop:
UIDragDetector(the deprecatedGuiObject.Draggableno longer works for new work).
World-space UI
BillboardGui(faces camera: names, health bars) — setMaxDistance,AlwaysOnTopsparingly, useSizein Scale for distance scaling;SurfaceGuifor screens/signs on parts. PreferAdornee+ keep them inPlayerGuifor per-player content. They stop rendering when their part streams out.ProximityPromptgives cross-platform interaction UI for free (validate on the server!).
Frameworks
For more than a few screens, a declarative framework keeps state and UI in sync:
- React (
jsdotlua/react+react-roblox) — components + hooks, familiar to web devs, scales to big apps. - Fusion — reactive state objects and declarative instance creation.
- Vide — small, fast reactive library. Match whatever the project already uses. Keep game state in controllers; UI only renders it and emits user intents.
Performance
- Avoid updating text/properties every frame when values haven't changed.
- Hide closed menus with
Visible = false(orEnabled = falseon the ScreenGui) instead of destroying/recreating them repeatedly; build large lists lazily or virtualize them. - Too many
UIGradient/CanvasGroup/UIStrokeon dynamic UI costs frame time; check the MicroProfiler.
Related skills
roblox-input (actions, gamepad), roblox-text-chat (filtering), roblox-monetization (shops),
roblox-luau, roblox-performance.