gamedev-skills/awesome-gamedev-agent-skills已通過檢查
SKILL DETAIL
roblox-ui
gamedev-skills/awesome-gamedev-agent-skills/roblox-ui
Build production Roblox interfaces with ScreenGui/PlayerGui lifecycle, responsive UDim2 layouts, safe insets, reusable editable Instances, cross-device input and selection, restrained motion, cleanup, and multi-viewport verification. Use for Roblox HUDs, inventories, shops, settings, ability bars, ScrollingFrames, AutomaticSize, GuiService, gamepad focus, touch UI, or UI that clips, resets on respawn, looks generic, or was generated as one giant LocalScript.
安裝量 · 225查看來源
Installation
npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill roblox-ui
技能檔案
SKILL.md
最近同步 · 2026年10月24日
agents/openai.yaml›
interface:
display_name: "Roblox UI"
short_description: "Build responsive production Roblox interfaces"
default_prompt: "Use $roblox-ui to implement and verify this Roblox interface."
assets/inventory-quality-test.model.json›
{
"ClassName": "ScreenGui",
"Properties": {
"ResetOnSpawn": false,
"ScreenInsets": "CoreUISafeInsets",
"ZIndexBehavior": "Sibling"
},
"Children": [
{
"Name": "Root",
"ClassName": "Frame",
"Properties": {
"AnchorPoint": [0.5, 0.5],
"Position": {"UDim2": [[0.5, 0], [0.5, 0]]},
"Size": {"UDim2": [[0.82, 0], [0.78, 0]]},
"BackgroundColor3": [0.0706, 0.0824, 0.102],
"BorderSizePixel": 0
},
"Children": [
{
"ClassName": "UISizeConstraint",
"Properties": {
"MinSize": [320, 360],
"MaxSize": [1120, 720]
}
},
{
"ClassName": "UICorner",
"Properties": {"CornerRadius": {"UDim": [0, 10]}}
},
{
"ClassName": "UIStroke",
"Properties": {
"Color": [0.1804, 0.2118, 0.2588],
"Thickness": 1,
"Transparency": 0.2
}
},
{
"ClassName": "UIPadding",
"Properties": {
"PaddingTop": {"UDim": [0, 20]},
"PaddingBottom": {"UDim": [0, 16]},
"PaddingLeft": {"UDim": [0, 20]},
"PaddingRight": {"UDim": [0, 20]}
}
},
{
"Name": "Header",
"ClassName": "Frame",
"Properties": {
"Size": {"UDim2": [[1, 0], [0, 56]]},
"BackgroundTransparency": 1
},
"Children": [
{
"Name": "Title",
"ClassName": "TextLabel",
"Properties": {
"Size": {"UDim2": [[0.65, 0], [1, 0]]},
"BackgroundTransparency": 1,
"Text": "INVENTORY",
"TextColor3": [0.9412, 0.949, 0.9647],
"TextSize": 24,
"Font": "GothamBold",
"TextXAlignment": "Left"
}
},
{
"Name": "Capacity",
"ClassName": "TextLabel",
"Properties": {
"AnchorPoint": [1, 0.5],
"Position": {"UDim2": [[1, 0], [0.5, 0]]},
"Size": {"UDim2": [[0, 120], [0, 28]]},
"BackgroundTransparency": 1,
"Text": "18 / 24 slots",
"TextColor3": [0.5686, 0.6157, 0.6902],
"TextSize": 14,
"Font": "Gotham",
"TextXAlignment": "Right"
}
}
]
},
{
"Name": "Content",
"ClassName": "Frame",
"Properties": {
"Position": {"UDim2": [[0, 0], [0, 64]]},
"Size": {"UDim2": [[1, 0], [1, -120]]},
"BackgroundTransparency": 1
},
"Children": [
{
"Name": "CategoryList",
"ClassName": "Frame",
"Properties": {
"Size": {"UDim2": [[0, 132], [1, 0]]},
"BackgroundTransparency": 1
},
"Children": [
{
"ClassName": "UIListLayout",
"Properties": {
"Padding": {"UDim": [0, 6]},
"SortOrder": "LayoutOrder"
}
},
{
"Name": "All",
"ClassName": "TextButton",
"Properties": {
"Size": {"UDim2": [[1, 0], [0, 38]]},
"BackgroundColor3": [0.1373, 0.1725, 0.2314],
"BorderSizePixel": 0,
"Text": "All items",
"TextColor3": [0.9412, 0.949, 0.9647],
"TextSize": 14,
"Font": "GothamMedium",
"TextXAlignment": "Left",
"SelectionOrder": 1
}
},
{
"Name": "Weapons",
"ClassName": "TextButton",
"Properties": {
"Size": {"UDim2": [[1, 0], [0, 38]]},
"BackgroundTransparency": 1,
"Text": "Weapons",
"TextColor3": [0.6745, 0.7098, 0.7647],
"TextSize": 14,
"Font": "Gotham",
"TextXAlignment": "Left",
"SelectionOrder": 2
}
},
{
"Name": "Consumables",
"ClassName": "TextButton",
"Properties": {
"Size": {"UDim2": [[1, 0], [0, 38]]},
"BackgroundTransparency": 1,
"Text": "Consumables",
"TextColor3": [0.6745, 0.7098, 0.7647],
"TextSize": 14,
"Font": "Gotham",
"TextXAlignment": "Left",
"SelectionOrder": 3
}
}
]
},
{
"Name": "ItemGrid",
"ClassName": "ScrollingFrame",
"Properties": {
"Position": {"UDim2": [[0, 148], [0, 0]]},
"Size": {"UDim2": [[0.58, -148], [1, 0]]},
"BackgroundTransparency": 1,
"BorderSizePixel": 0,
"AutomaticCanvasSize": "Y",
"CanvasSize": {"UDim2": [[0, 0], [0, 0]]},
"ScrollBarThickness": 4
},
"Children": [
{
"ClassName": "UIPadding",
"Properties": {
"PaddingTop": {"UDim": [0, 4]},
"PaddingBottom": {"UDim": [0, 4]},
"PaddingLeft": {"UDim": [0, 4]},
"PaddingRight": {"UDim": [0, 4]}
}
},
{
"ClassName": "UIGridLayout",
"Properties": {
"CellSize": {"UDim2": [[0.5, -6], [0, 112]]},
"CellPadding": {"UDim2": [[0, 12], [0, 12]]},
"SortOrder": "LayoutOrder"
}
},
{
"Name": "FieldKnife",
"ClassName": "TextButton",
"Properties": {
"BackgroundColor3": [0.102, 0.1216, 0.1529],
"BorderSizePixel": 0,
"Text": "FIELD KNIFE\nLight · Equipped",
"TextColor3": [0.9176, 0.9294, 0.949],
"TextSize": 14,
"Font": "GothamMedium",
"TextWrapped": true,
"SelectionOrder": 10
},
"Children": [
{
"ClassName": "UIStroke",
"Properties": {"Color": [0.2902, 0.5137, 0.7765], "Thickness": 2}
}
]
},
{
"Name": "Medkit",
"ClassName": "TextButton",
"Properties": {
"BackgroundColor3": [0.102, 0.1216, 0.1529],
"BorderSizePixel": 0,
"Text": "MEDKIT\nRestores 50 health",
"TextColor3": [0.9176, 0.9294, 0.949],
"TextSize": 14,
"Font": "GothamMedium",
"TextWrapped": true,
"SelectionOrder": 11
}
},
{
"Name": "SignalFlare",
"ClassName": "TextButton",
"Properties": {
"BackgroundColor3": [0.102, 0.1216, 0.1529],
"BorderSizePixel": 0,
"Text": "SIGNAL FLARE\n3 remaining",
"TextColor3": [0.9176, 0.9294, 0.949],
"TextSize": 14,
"Font": "GothamMedium",
"TextWrapped": true,
"SelectionOrder": 12
}
},
{
"Name": "Rope",
"ClassName": "TextButton",
"Properties": {
"BackgroundColor3": [0.102, 0.1216, 0.1529],
"BorderSizePixel": 0,
"Text": "CLIMBING ROPE\nUtility",
"TextColor3": [0.9176, 0.9294, 0.949],
"TextSize": 14,
"Font": "GothamMedium",
"TextWrapped": true,
"SelectionOrder": 13
}
}
]
},
{
"Name": "DetailsPanel",
"ClassName": "Frame",
"Properties": {
"AnchorPoint": [1, 0],
"Position": {"UDim2": [[1, 0], [0, 0]]},
"Size": {"UDim2": [[0.38, -12], [1, 0]]},
"BackgroundColor3": [0.0902, 0.1059, 0.1333],
"BorderSizePixel": 0
},
"Children": [
{
"ClassName": "UIPadding",
"Properties": {
"PaddingTop": {"UDim": [0, 18]},
"PaddingBottom": {"UDim": [0, 18]},
"PaddingLeft": {"UDim": [0, 18]},
"PaddingRight": {"UDim": [0, 18]}
}
},
{
"Name": "ItemName",
"ClassName": "TextLabel",
"Properties": {
"Size": {"UDim2": [[1, 0], [0, 32]]},
"BackgroundTransparency": 1,
"Text": "Field Knife",
"TextColor3": [0.9412, 0.949, 0.9647],
"TextSize": 20,
"Font": "GothamBold",
"TextXAlignment": "Left"
}
},
{
"Name": "Description",
"ClassName": "TextLabel",
"Properties": {
"Position": {"UDim2": [[0, 0], [0, 44]]},
"Size": {"UDim2": [[1, 0], [0, 92]]},
"BackgroundTransparency": 1,
"Text": "A balanced utility blade. Quick to ready and quiet in close quarters.",
"TextColor3": [0.6745, 0.7098, 0.7647],
"TextSize": 14,
"Font": "Gotham",
"TextWrapped": true,
"TextXAlignment": "Left",
"TextYAlignment": "Top"
}
},
{
"Name": "Equip",
"ClassName": "TextButton",
"Properties": {
"AnchorPoint": [0, 1],
"Position": {"UDim2": [[0, 0], [1, 0]]},
"Size": {"UDim2": [[1, 0], [0, 44]]},
"BackgroundColor3": [0.2902, 0.5137, 0.7765],
"BorderSizePixel": 0,
"Text": "Equip item",
"TextColor3": [1, 1, 1],
"TextSize": 15,
"Font": "GothamBold",
"SelectionOrder": 20
},
"Children": [
{"ClassName": "UICorner", "Properties": {"CornerRadius": {"UDim": [0, 6]}}}
]
}
]
}
]
},
{
"Name": "Footer",
"ClassName": "TextLabel",
"Properties": {
"AnchorPoint": [0, 1],
"Position": {"UDim2": [[0, 0], [1, 0]]},
"Size": {"UDim2": [[1, 0], [0, 36]]},
"BackgroundTransparency": 1,
"Text": "A Select B Close LB/RB Categories",
"TextColor3": [0.4902, 0.5373, 0.6078],
"TextSize": 13,
"Font": "Gotham",
"TextXAlignment": "Left"
}
}
]
}
]
}
assets/inventory-quality-test.project.json›
{
"name": "RobloxUIQualityTest",
"tree": {
"$className": "DataModel",
"StarterGui": {
"$className": "StarterGui",
"InventoryGui": {
"$path": "inventory-quality-test.model.json"
}
}
}
}
references/input-navigation.md›
# Roblox UI input, selection, and motion
Read this for gamepad navigation, touch/mouse parity, modal focus, `ContextActionService`, or UI
motion. Targets Roblox's rolling platform APIs.
## Control contract
- Prefer `GuiButton.Activated` for the primary action because it unifies mouse, touch, and gamepad.
- Give every interactive state a visual response: hover where a pointer exists, pressed, selected,
disabled, and busy if an operation can take time. Do not communicate state with color alone.
- Make touch targets comfortably hittable and separated; do not add invisible hit zones that overlap
neighboring controls.
- Show input prompts that match the active device, but never hide the only explanation of an action
during device switching.
## Selection graph
On screen open, select the most likely safe action—not a destructive action. Use
`GuiService.SelectedObject`. Let automatic selection handle simple lists; assign
`NextSelectionLeft/Right/Up/Down` where layout geometry is ambiguous. Test every edge and a full
round trip through the graph.
Set initial selection only after the control is rendered and only for keyboard/gamepad navigation.
Device emulation can expose keyboard capability while touch controls are active, so prefer the
active input policy used by the project; do not force a selected object on a touch-first screen.
Inspect the default adornment. A custom `SelectionImageObject` must remain a valid visible
`GuiObject` (it can live offscreen as the reusable template) and should match the interface's focus
treatment instead of adding a large glow.
When a modal opens:
1. remember the prior selected object if it still belongs to the underlying screen;
2. prevent interaction with the screen beneath it;
3. select the modal's first safe control;
4. keep directional navigation inside the modal;
5. on close, restore the prior object or a stable fallback.
To keep directional navigation inside the modal, set `GuiBase2d.SelectionGroup = true` on the modal
root and set its `SelectionBehaviorUp`/`Down`/`Left`/`Right` to `Enum.SelectionBehavior.Stop`. The
older `GuiService:AddSelectionParent()`/`AddSelectionTuple()`/`RemoveSelectionGroup()` methods are
all deprecated — do not use them in new work.
## Context actions
Use `ContextActionService:BindAction()` for screen-level actions such as close/back, tab change, or
an ability-bar shortcut that is not already owned by a button. Bind on screen activation and
`UnbindAction()` on close. Return `Enum.ContextActionResult.Sink` only when the UI intentionally
consumes the action; otherwise allow gameplay to receive it.
Do not create a second input system inside a menu. Coordinate screen actions with `input-systems`
and preserve the project's existing action names and bindings.
For cross-device action binding defined at edit time, the Input Action System
(`InputContext`/`InputAction`/`InputBinding`) is the modern layer: it maps one action to
keyboard, gamepad, and touch at once, and an `InputContext` can be enabled/disabled (with
`Priority`/`Sink`) so a menu's actions replace gameplay actions while it is open. Use it for
screen-level action binding and cross-device parity; keep `GuiService.SelectedObject` and the
neighbor graph for moving UI focus itself.
## Motion lifecycle
- Read `GuiService.ReducedMotionEnabled`; replace spatial movement with an instant state or short
opacity change when reduced motion is requested.
- Reuse a small timing/easing vocabulary. Typical state transitions should be quick enough not to
delay interaction.
- Cancel or supersede an in-flight tween when the target state changes. Do not queue contradictory
open/close animations.
- Animate a stable parent (`CanvasGroup` is useful for group transparency) instead of spawning a
tween for every descendant.
- The logical state changes immediately; animation visualizes it. Never make server-critical state
depend on tween completion.
## Controller verification
Use Studio's Controller Emulator and also a real controller when available. Verify initial focus,
all directions, activation, back/cancel, tab/shoulder navigation if used, scroll-following selected
items, focus restoration, and mouse-to-gamepad switching. Controller emulation proves mapping and
navigation, not TV legibility; use Device Emulator/console sizing for that.
## Primary references
- `https://create.roblox.com/docs/reference/engine/classes/GuiService`
- `https://create.roblox.com/docs/studio/testing-modes`
- `https://create.roblox.com/docs/input/input-action-system`
- `https://create.roblox.com/docs/production/publishing/console-guidelines`
references/responsive-layout.md›
# Responsive Roblox UI
Read this when a screen must adapt across viewport sizes or when layout defects appear outside the
author's primary monitor. This reference targets Roblox's rolling platform APIs.
## Choose structure before coordinates
1. Identify persistent regions: HUD edge groups, header, primary content, secondary detail,
footer/action row, modal layer.
2. Decide what changes on narrow screens. Reflow or collapse secondary regions; do not only scale
the desktop composition down.
3. Anchor edge-owned regions to their edge (`AnchorPoint` must match `Position`). Put siblings in
layouts instead of manually positioning each child.
4. Use offsets for bounded component dimensions and spacing; use scale for proportional placement
and flexible region size. Inspect `AbsoluteSize`, not device names, for layout breakpoints.
## Roblox layout primitives
| Need | Primitive | Check |
|---|---|---|
| vertical/horizontal flow | `UIListLayout` | `Padding`, alignment, flex behavior, content size |
| distribute a row/column's spare space | `UIListLayout` `HorizontalFlex`/`VerticalFlex` | set fill direction first; `UIFlexAlignment` Fill/SpaceBetween/SpaceAround/SpaceEvenly |
| align or stretch items across their line | `UIListLayout` `ItemLineAlignment` | Stretch fills the cross-axis; aligns uneven item heights/widths |
| make one item absorb the variable space | `UIFlexItem` (`FlexMode`) | child of the flexing item; `UIFlexMode` Fill/Grow/Shrink/Custom |
| uniform collection | `UIGridLayout` | cell size at narrow/wide bounds; scroll canvas |
| true rows and columns | `UITableLayout` | headers and cell alignment; avoid for card collections |
| inner spacing | `UIPadding` | use one spacing scale; include it in width calculations |
| content-driven object | `AutomaticSize` | localization, wrapping, and circular dependencies |
| content-driven scrolling | `AutomaticCanvasSize` | scrolling direction and bottom reachability |
| proportional shape/media | `UIAspectRatioConstraint` | avoid constraining text-heavy containers |
| min/max component size | `UISizeConstraint` | test both limits, not only preferred width |
| bounded text scaling | `UITextSizeConstraint` | still test localization and TV distance |
| deliberate subtree zoom | `UIScale` | avoid using one scale as the entire responsive system |
### Flex distribution
Flex lets a `UIListLayout` share a row or column's spare space without manual
`AbsoluteSize` math. Set the fill direction, then set `HorizontalFlex` (or `VerticalFlex`)
to a `UIFlexAlignment` value to distribute space along that axis, and `ItemLineAlignment`
to align or stretch items across the line. To make one specific item absorb the variable
space (fixed labels at both ends, a flexible bar between), parent a `UIFlexItem` to that
item and set its `FlexMode`.
```lua
-- Tab bar whose tabs share the width equally, whatever the count:
tabBar.UIListLayout.FillDirection = Enum.FillDirection.Horizontal
tabBar.UIListLayout.HorizontalFlex = Enum.UIFlexAlignment.Fill
-- Slider row: the labels keep their size, the track fills the gap between them.
local grow = Instance.new("UIFlexItem")
grow.FlexMode = Enum.UIFlexMode.Fill
grow.Parent = sliderTrack -- a sibling under the row's UIListLayout
```
Prefer a `UIGridLayout` when cells must align strictly in both axes; reach for flex only
where a row/column genuinely needs variable distribution, since it adds a small layout
cost when resizing or adding/removing items.
## Safe areas and reserved controls
For interactive `ScreenGui` content, retain `ScreenInsets = CoreUISafeInsets` unless the design
has a measured reason not to. Decorative backgrounds may intentionally bleed into `None`, but put
interactive descendants in a separate inset-aware `ScreenGui` or safe root.
`GuiService:GetInsetArea(Enum.ScreenInsets.CoreUISafeInsets)` returns the actual inset rectangle when
custom positioning needs it — the `ScreenInsets` argument is required, and which inset set you ask
for is the point of the call. `GuiService.TopbarInset` gives the topbar rectangle on its own.
Mobile tests must include the bottom-left movement control and bottom-right jump/action region.
Console tests must consider viewing distance and overscan, not just a desktop window enlarged to TV
resolution.
Do not choose breakpoints from device labels. Choose them where the composition loses usable width:
for example, a sidebar + grid + detail view must switch to a horizontal category strip, full-width
grid, and separate detail state before any region becomes token-width. On touch, relocate bottom
actions away from reserved controls and remove irrelevant controller hint rows.
## Density and visual rhythm
Derive a small spacing scale from the existing game (for example 4/8/12/16/24, not a mandated
universal scale). Reuse it for padding, gaps, icon-to-label space, and section separation. A larger
gap signals a stronger grouping boundary. Choose text roles (title, section, body, metadata) and
keep role sizes/weights stable. Use consistent icon boxes even when source art has different bounds.
## Required verification matrix
| Configuration | Inspect |
|---|---|
| desktop, mouse + keyboard | hover/pressed, resizing, ultrawide or tall extreme |
| narrow mobile portrait | reflow, touch reach, wrapping, scrolling, safe cutouts |
| mobile landscape | reserved controls, low vertical space, modal height |
| tablet-like | density and excessive empty space |
| console/gamepad | initial focus, every directional edge, back action, TV readability |
For every configuration, exercise open/close, content-empty/content-full, long localized-like text,
disabled and error states, scrolling to both ends, and character respawn. Record observed defects
and the exact change that fixed them.
## Bundled Instance fixture
`../assets/inventory-quality-test.project.json` maps the adjacent Rojo JSON model into `StarterGui`.
Build it with `rojo build assets/inventory-quality-test.project.json -o inventory-test.rbxlx` from
the skill directory. The result contains a real `ScreenGui` hierarchy—not a procedural UI script.
Inspect it in Explorer, then use it only to exercise the matrix above. It deliberately uses
restrained geometry and a clear category/grid/detail hierarchy; adaptation logic, interaction
behavior, data population, and project styling remain work for the test scenario. Do not ship the
fixture unchanged.
## Primary references
- `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/ui/list-flex-layouts`
- `https://create.roblox.com/docs/production/publishing/adaptive-design`
SKILL.md›
---
name: roblox-ui
description: >
Build production Roblox interfaces with ScreenGui/PlayerGui lifecycle, responsive UDim2
layouts, safe insets, reusable editable Instances, cross-device input and selection, restrained
motion, cleanup, and multi-viewport verification. Use for Roblox HUDs, inventories, shops,
settings, ability bars, ScrollingFrames, AutomaticSize, GuiService, gamepad focus, touch UI, or
UI that clips, resets on respawn, looks generic, or was generated as one giant LocalScript.
---
# Roblox UI
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.
## When to use
- Use for authored `ScreenGui` interfaces, Roblox layout/constraint APIs, UI lifecycle, input,
focus, animation, performance, or responsive/device fixes.
- Use to replace brittle pixel layouts, runtime-created Instance tangles, mouse-only menus, or
generic generated styling with a maintainable hierarchy.
**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`.
## Workflow
1. **Inspect before designing.** In Explorer, inventory existing `ScreenGui`s, UI modules,
fonts, colors, icon family, spacing, corners, strokes, animation timing, naming, and reusable
components. Capture representative screenshots at two viewports. Extend that language.
2. **Define ownership and lifecycle.** `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.
3. **Author a readable hierarchy.** Prefer meaningful editable Instances and small behavior
modules. A normal screen should be understandable in Explorer; procedural generation is for
genuinely data-driven repetition, not the whole shell.
4. **Build responsive structure.** Combine `UDim2` scale for placement with bounded offsets,
correct `AnchorPoint`s, layouts, padding, content sizing, and constraints. Choose explicit
narrow/wide arrangements instead of shrinking a desktop panel until it technically fits.
5. **Handle platform obstructions.** Keep interactive UI in `CoreUISafeInsets` (the recommended
`ScreenGui.ScreenInsets` default), and account for touch controls and TV distance/overscan.
6. **Wire state and input.** Render from a small view state; update on events. Use `Activated` for
`GuiButton`s, explicit selected states, and reachable gamepad navigation. Bind non-widget
actions with `ContextActionService` only while the screen owns them.
7. **Polish with restraint.** Implement hover, pressed, selected, and disabled states. Animate
state changes, not decoration; honor `GuiService.ReducedMotionEnabled` and clean up tweens and
connections when the screen closes or is replaced.
8. **Verify the real interface.** Use Device Emulator and Controller Emulator. Test desktop,
narrow portrait, mobile landscape, tablet-like, and console/gamepad; then respawn. Record
clipping, overlap, text, scroll, safe-area, focus path, touch target, and lifecycle results.
## Production structure
```text
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.
## Patterns
### Layout that survives content and viewport changes
```lua
-- 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.
### One activation path and explicit gamepad entry
```lua
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.
For cross-device action binding, the Input Action System (`InputContext`/`InputAction`/
`InputBinding`) is the modern edit-time layer; see `references/input-navigation.md`.
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.
### State transition without animation noise
```lua
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.
## Visual quality gates
- Establish hierarchy through size, weight, contrast, grouping, and whitespace before adding
decoration. Use a small spacing scale and align to it.
- Preserve the game's fonts, palette, icons, radii, and strokes. If none exist, define a compact
token module; do not improvise per component.
- Use corners where the material/form calls for them, not on every nested `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.
- Buttons with different importance should not look identical. Define primary, secondary,
quiet, destructive, selected, pressed, hover, and disabled treatment only as needed.
- Motion communicates open/close, focus, confirmation, or spatial relationship. Keep it short,
interruptible, and subtle; avoid perpetual motion and decorative tween chains.
- On touch devices, reserve the default movement/jump corners. Move or recompose bottom actions
that overlap them, and hide keyboard/gamepad hints when those inputs are not active.
## Common failures
| 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 |
## Resources
- Read `references/responsive-layout.md` for layout decisions, inset policy, density, and the
required viewport matrix.
- Read `references/input-navigation.md` when implementing gamepad/touch focus, modal behavior,
action binding, or motion preferences.
- Build `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.
## Related skills
- `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.
## Primary references
- Roblox Creator Hub: `https://create.roblox.com/docs/ui/on-screen-containers`
- Roblox Creator Hub: `https://create.roblox.com/docs/ui/position-and-size`
- Roblox Creator Hub: `https://create.roblox.com/docs/ui/size-modifiers`
- Roblox Creator Hub: `https://create.roblox.com/docs/studio/testing-modes`