Implement deliberate, editable Roblox UI that preserves the experience's visual language and
works across supported devices. Targets Roblox's rolling platform APIs. Pair with game-ui-ux
for engine-neutral information architecture and UX; this skill owns Roblox Instances and APIs.
ScreenGui interfaces, Roblox layout/constraint APIs, UI lifecycle, input,
focus, animation, performance, or responsive/device fixes.When not to use: general UX flow and accessibility principles belong to game-ui-ux; raw
gameplay action mapping belongs to input-systems; server trust belongs to roblox-networking.
ScreenGuis, UI modules,
fonts, colors, icon family, spacing, corners, strokes, animation timing, naming, and reusable
components. Capture representative screenshots at two viewports. Extend that language.StarterGui is the authored template; Roblox clones it
into each player's PlayerGui. Decide whether each ScreenGui should persist across respawn
with ResetOnSpawn = false; never keep stale references to a destroyed clone.UDim2 scale for placement with bounded offsets,
correct AnchorPoints, layouts, padding, content sizing, and constraints. Choose explicit
narrow/wide arrangements instead of shrinking a desktop panel until it technically fits.CoreUISafeInsets (the recommended
ScreenGui.ScreenInsets default), and account for touch controls and TV distance/overscan.Activated for
GuiButtons, explicit selected states, and reachable gamepad navigation. Bind non-widget
actions with ContextActionService only while the screen owns them.GuiService.ReducedMotionEnabled and clean up tweens and
connections when the screen closes or is replaced.StarterGui
└── InventoryGui (ScreenGui; ResetOnSpawn = false)
├── Root (Frame)
│ ├── Header
│ ├── Content
│ │ ├── CategoryList
│ │ ├── ItemGrid (ScrollingFrame)
│ │ └── DetailsPanel
│ └── Footer
└── InventoryController (LocalScript)
ReplicatedStorage
└── UI
├── Components
└── Theme
Names describe roles, not colors or temporary positions. Keep repeated item cells as templates or components, but keep the authored shell visible in Explorer.
-- Authored properties on ItemGrid (ScrollingFrame):
-- AutomaticCanvasSize = Enum.AutomaticSize.Y
-- CanvasSize = UDim2.fromOffset(0, 0)
-- Children: UIPadding + UIGridLayout
local grid = itemGrid.UIGridLayout
local projectBreakpoint = 700 -- derive from where this composition stops being usable
local function applyComposition()
local narrow = root.AbsoluteSize.X < projectBreakpoint
categoryList.UIListLayout.FillDirection = if narrow
then Enum.FillDirection.Horizontal
else Enum.FillDirection.Vertical
categoryList.Size = if narrow
then UDim2.new(1, 0, 0, 40)
else UDim2.new(0, 148, 1, 0)
itemGrid.Position = if narrow then UDim2.fromOffset(0, 52) else UDim2.fromOffset(164, 0)
itemGrid.Size = if narrow
then UDim2.new(1, 0, 1, -52)
else UDim2.new(0.58, -164, 1, 0)
detailsPanel.Visible = not narrow -- open details as a separate narrow-screen state
local width = itemGrid.AbsoluteSize.X
local columns = if width < 520 then 2 elseif width < 820 then 3 else 4
local gap = 12
local usable = width - itemGrid.UIPadding.PaddingLeft.Offset
- itemGrid.UIPadding.PaddingRight.Offset - gap * (columns - 1)
grid.CellSize = UDim2.fromOffset(math.floor(usable / columns), 112)
grid.CellPadding = UDim2.fromOffset(gap, gap)
end
root:GetPropertyChangedSignal("AbsoluteSize"):Connect(applyComposition)
applyComposition()
Use UIListLayout for one-dimensional flow, UIGridLayout for uniform cells, and
UITableLayout only when row/column alignment is truly tabular. AutomaticSize fits content;
AutomaticCanvasSize fits scroll content. Bound extremes with UISizeConstraint,
UIAspectRatioConstraint, or UITextSizeConstraint rather than one global UIScale guess.
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local function openInventory()
root.Visible = true
task.spawn(function()
RunService.RenderStepped:Wait() -- selection must be rendered and selectable
if root.Visible and (UserInputService.GamepadEnabled
or (UserInputService.KeyboardEnabled and not UserInputService.TouchEnabled)) then
GuiService.SelectedObject = firstItemButton
end
end)
end
local function closeInventory()
GuiService.SelectedObject = nil
root.Visible = false
end
firstItemButton.Activated:Connect(function()
selectItem(firstItemButton:GetAttribute("ItemId"))
end)
Prefer Activated over separate mouse/touch handlers. Set NextSelectionUp/Down/Left/Right
when automatic navigation is ambiguous, especially around grids, sidebars, and modals. A modal
must trap focus inside itself and restore a sensible selection when it closes.
Inspect Roblox's default selection adornment in context. If it overwhelms the interface, assign a
project-owned SelectionImageObject or selected-state treatment; keep it visible, valid, and
restrained rather than disabling focus feedback.
local TweenService = game:GetService("TweenService")
local GuiService = game:GetService("GuiService")
local fade = TweenInfo.new(0.14, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local function setPanelVisible(visible: boolean)
panel.Visible = true -- panel is a CanvasGroup
if GuiService.ReducedMotionEnabled then
panel.GroupTransparency = if visible then 0 else 1
else
TweenService:Create(panel, fade, {
GroupTransparency = if visible then 0 else 1,
}):Play()
end
if not visible then
task.delay(fade.Time, function()
if panel.GroupTransparency == 1 then panel.Visible = false end
end)
end
end
Animate a panel or state boundary once. Do not independently bounce every descendant.
Frame. Avoid nested
card-on-card shells, universal pills, arbitrary gradients/glows/shadows, neon outlines,
excessive transparency/blur, oversized headings, meaningless icons, and huge empty panels.| Symptom | Likely cause | Remedy |
|---|---|---|
| UI disappears or duplicates after respawn | wrong ResetOnSpawn policy or stale clone |
choose policy; reacquire from PlayerGui; clean old bindings |
| Phone layout clips | desktop offsets/global scale used as layout | add narrow composition, layouts, bounds, and Device Emulator checks |
| Content hidden under topbar/notch | insets ignored or ScreenInsets = None |
use CoreUISafeInsets for interactive UI; inspect intentional bleed |
| Scroll ends early | fixed CanvasSize |
use AutomaticCanvasSize or layout AbsoluteContentSize |
| Controller gets stuck | no entry selection or bad neighbor graph | set initial SelectedObject; define and test focus edges/modal trap |
| UI looks generated | decoration substituted for hierarchy | remove effects; align, reduce nesting, fix density and component roles |
| Explorer contains only one script | entire authored interface is procedural | author the shell as named Instances; generate repeated content only |
| Increasing connection/tween count | screen rebinds without cleanup | own connections per screen lifecycle; disconnect/cancel on teardown |
references/responsive-layout.md for layout decisions, inset policy, density, and the
required viewport matrix.references/input-navigation.md when implementing gamepad/touch focus, modal behavior,
action binding, or motion preferences.assets/inventory-quality-test.project.json with Rojo (it maps the adjacent JSON model)
for an editable Instance fixture used in visual/layout review; it is a test baseline, not a style
template for a shipping game.game-ui-ux — information hierarchy, screen flow, accessibility, and UX validation.input-systems — gameplay action mapping and device switching.roblox-networking — server-authoritative requests initiated by UI.roblox-studio-workflow — Explorer-first editing and Studio verification.https://create.roblox.com/docs/ui/on-screen-containers
https://create.roblox.com/docs/ui/position-and-size
https://create.roblox.com/docs/ui/size-modifiers
https://create.roblox.com/docs/studio/testing-modes