roblox-opencode 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +112 -122
  2. package/commands/setup-game.md +108 -108
  3. package/commands/sync-check.md +53 -53
  4. package/core/roblox-core.md +93 -93
  5. package/dist/server.js +189 -167
  6. package/package.json +35 -35
  7. package/skills/roblox-analytics/SKILL.md +277 -277
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -75
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
  10. package/skills/roblox-architecture/SKILL.md +863 -863
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
  12. package/skills/roblox-code-review/SKILL.md +686 -686
  13. package/skills/roblox-data/SKILL.md +889 -889
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -1729
  15. package/skills/roblox-debug/SKILL.md +98 -98
  16. package/skills/roblox-gui/SKILL.md +1103 -1103
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -150
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -411
  21. package/skills/roblox-luau-mastery/SKILL.md +1519 -1519
  22. package/skills/roblox-monetization/SKILL.md +1084 -1084
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -131
  24. package/skills/roblox-networking/SKILL.md +669 -669
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -193
  26. package/skills/roblox-publish-checklist/SKILL.md +127 -127
  27. package/skills/roblox-runtime/SKILL.md +753 -753
  28. package/skills/roblox-sharp-edges/SKILL.md +294 -294
  29. package/skills/roblox-sync/SKILL.md +126 -126
  30. package/skills/roblox-testing/SKILL.md +943 -943
  31. package/skills/roblox-tooling/SKILL.md +149 -149
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
  34. package/vendor/LICENSES/promise-LICENSE +20 -20
  35. package/vendor/LICENSES/t-LICENSE +21 -21
  36. package/vendor/LICENSES/testez-LICENSE +200 -200
  37. package/vendor/README.md +83 -83
  38. package/vendor/fusion/Animation/ExternalTime.luau +83 -83
  39. package/vendor/fusion/Animation/Spring.luau +321 -321
  40. package/vendor/fusion/Animation/Stopwatch.luau +127 -127
  41. package/vendor/fusion/Animation/Tween.luau +187 -187
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
  44. package/vendor/fusion/Animation/lerpType.luau +163 -163
  45. package/vendor/fusion/Animation/packType.luau +99 -99
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -80
  47. package/vendor/fusion/Animation/unpackType.luau +102 -102
  48. package/vendor/fusion/Colour/Oklab.luau +70 -70
  49. package/vendor/fusion/Colour/sRGB.luau +54 -54
  50. package/vendor/fusion/External.luau +167 -167
  51. package/vendor/fusion/ExternalDebug.luau +69 -69
  52. package/vendor/fusion/Graph/Observer.luau +113 -113
  53. package/vendor/fusion/Graph/castToGraph.luau +28 -28
  54. package/vendor/fusion/Graph/change.luau +80 -80
  55. package/vendor/fusion/Graph/depend.luau +32 -32
  56. package/vendor/fusion/Graph/evaluate.luau +55 -55
  57. package/vendor/fusion/Instances/Attribute.luau +57 -57
  58. package/vendor/fusion/Instances/AttributeChange.luau +46 -46
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -63
  60. package/vendor/fusion/Instances/Child.luau +21 -21
  61. package/vendor/fusion/Instances/Children.luau +147 -147
  62. package/vendor/fusion/Instances/Hydrate.luau +32 -32
  63. package/vendor/fusion/Instances/New.luau +52 -52
  64. package/vendor/fusion/Instances/OnChange.luau +49 -49
  65. package/vendor/fusion/Instances/OnEvent.luau +53 -53
  66. package/vendor/fusion/Instances/Out.luau +69 -69
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -194
  69. package/vendor/fusion/LICENSE +21 -21
  70. package/vendor/fusion/Logging/formatError.luau +48 -48
  71. package/vendor/fusion/Logging/messages.luau +51 -51
  72. package/vendor/fusion/Logging/parseError.luau +24 -24
  73. package/vendor/fusion/Memory/checkLifetime.luau +133 -133
  74. package/vendor/fusion/Memory/deriveScope.luau +23 -23
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
  76. package/vendor/fusion/Memory/doCleanup.luau +78 -78
  77. package/vendor/fusion/Memory/innerScope.luau +33 -33
  78. package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
  79. package/vendor/fusion/Memory/needsDestruction.luau +16 -16
  80. package/vendor/fusion/Memory/poisonScope.luau +33 -33
  81. package/vendor/fusion/Memory/scopePool.luau +54 -54
  82. package/vendor/fusion/Memory/scoped.luau +26 -26
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
  84. package/vendor/fusion/RobloxExternal.luau +97 -97
  85. package/vendor/fusion/State/Computed.luau +138 -138
  86. package/vendor/fusion/State/For/Disassembly.luau +210 -210
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -30
  88. package/vendor/fusion/State/For/init.luau +109 -109
  89. package/vendor/fusion/State/ForKeys.luau +93 -93
  90. package/vendor/fusion/State/ForPairs.luau +96 -96
  91. package/vendor/fusion/State/ForValues.luau +93 -93
  92. package/vendor/fusion/State/Value.luau +87 -87
  93. package/vendor/fusion/State/castToState.luau +25 -25
  94. package/vendor/fusion/State/peek.luau +30 -30
  95. package/vendor/fusion/Types.luau +314 -314
  96. package/vendor/fusion/Utility/Contextual.luau +90 -90
  97. package/vendor/fusion/Utility/Safe.luau +22 -22
  98. package/vendor/fusion/Utility/isSimilar.luau +29 -29
  99. package/vendor/fusion/Utility/merge.luau +35 -35
  100. package/vendor/fusion/Utility/nameOf.luau +34 -34
  101. package/vendor/fusion/Utility/never.luau +13 -13
  102. package/vendor/fusion/Utility/nicknames.luau +10 -10
  103. package/vendor/fusion/Utility/xtypeof.luau +26 -26
  104. package/vendor/fusion/init.luau +82 -82
  105. package/vendor/profilestore/init.luau +2242 -2242
  106. package/vendor/promise/init.luau +1982 -1982
  107. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
  108. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
  109. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
  110. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
  111. package/vendor/rbxutil/buffer-util/Types.luau +60 -60
  112. package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
  113. package/vendor/rbxutil/buffer-util/init.luau +41 -41
  114. package/vendor/rbxutil/buffer-util/package.json +16 -16
  115. package/vendor/rbxutil/buffer-util/wally.toml +9 -9
  116. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
  117. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
  118. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
  119. package/vendor/rbxutil/comm/Client/init.luau +135 -135
  120. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
  121. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
  122. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
  123. package/vendor/rbxutil/comm/Server/init.luau +140 -140
  124. package/vendor/rbxutil/comm/Types.luau +18 -18
  125. package/vendor/rbxutil/comm/Util.luau +27 -27
  126. package/vendor/rbxutil/comm/init.luau +35 -35
  127. package/vendor/rbxutil/comm/wally.toml +13 -13
  128. package/vendor/rbxutil/component/init.luau +759 -759
  129. package/vendor/rbxutil/component/init.test.luau +311 -311
  130. package/vendor/rbxutil/component/wally.toml +14 -14
  131. package/vendor/rbxutil/concur/init.luau +542 -542
  132. package/vendor/rbxutil/concur/init.test.luau +364 -364
  133. package/vendor/rbxutil/concur/wally.toml +8 -8
  134. package/vendor/rbxutil/enum-list/init.luau +101 -101
  135. package/vendor/rbxutil/enum-list/init.test.luau +91 -91
  136. package/vendor/rbxutil/enum-list/wally.toml +8 -8
  137. package/vendor/rbxutil/find/index.d.ts +20 -20
  138. package/vendor/rbxutil/find/init.luau +44 -44
  139. package/vendor/rbxutil/find/package.json +17 -17
  140. package/vendor/rbxutil/find/wally.toml +8 -8
  141. package/vendor/rbxutil/input/Gamepad.luau +559 -559
  142. package/vendor/rbxutil/input/Keyboard.luau +124 -124
  143. package/vendor/rbxutil/input/Mouse.luau +278 -278
  144. package/vendor/rbxutil/input/PreferredInput.luau +91 -91
  145. package/vendor/rbxutil/input/Touch.luau +120 -120
  146. package/vendor/rbxutil/input/init.luau +33 -33
  147. package/vendor/rbxutil/input/wally.toml +12 -12
  148. package/vendor/rbxutil/loader/index.d.ts +15 -15
  149. package/vendor/rbxutil/loader/init.luau +137 -137
  150. package/vendor/rbxutil/loader/wally.toml +8 -8
  151. package/vendor/rbxutil/log/index.d.ts +38 -38
  152. package/vendor/rbxutil/log/init.luau +746 -746
  153. package/vendor/rbxutil/log/wally.toml +8 -8
  154. package/vendor/rbxutil/net/init.luau +190 -190
  155. package/vendor/rbxutil/net/wally.toml +8 -8
  156. package/vendor/rbxutil/option/index.d.ts +44 -44
  157. package/vendor/rbxutil/option/init.luau +489 -489
  158. package/vendor/rbxutil/option/init.test.luau +342 -342
  159. package/vendor/rbxutil/option/wally.toml +8 -8
  160. package/vendor/rbxutil/pid/index.d.ts +53 -53
  161. package/vendor/rbxutil/pid/init.luau +195 -195
  162. package/vendor/rbxutil/pid/package.json +16 -16
  163. package/vendor/rbxutil/pid/wally.toml +9 -9
  164. package/vendor/rbxutil/quaternion/index.d.ts +117 -117
  165. package/vendor/rbxutil/quaternion/init.luau +570 -570
  166. package/vendor/rbxutil/quaternion/package.json +16 -16
  167. package/vendor/rbxutil/quaternion/wally.toml +9 -9
  168. package/vendor/rbxutil/query/index.d.ts +43 -43
  169. package/vendor/rbxutil/query/init.luau +117 -117
  170. package/vendor/rbxutil/query/package.json +18 -18
  171. package/vendor/rbxutil/query/wally.toml +9 -9
  172. package/vendor/rbxutil/sequent/index.d.ts +28 -28
  173. package/vendor/rbxutil/sequent/init.luau +340 -340
  174. package/vendor/rbxutil/sequent/package.json +16 -16
  175. package/vendor/rbxutil/sequent/wally.toml +9 -9
  176. package/vendor/rbxutil/ser/init.luau +175 -175
  177. package/vendor/rbxutil/ser/init.test.luau +50 -50
  178. package/vendor/rbxutil/ser/wally.toml +11 -11
  179. package/vendor/rbxutil/shake/index.d.ts +36 -36
  180. package/vendor/rbxutil/shake/init.luau +532 -532
  181. package/vendor/rbxutil/shake/init.test.luau +267 -267
  182. package/vendor/rbxutil/shake/package.json +16 -16
  183. package/vendor/rbxutil/shake/wally.toml +9 -9
  184. package/vendor/rbxutil/signal/index.d.ts +100 -100
  185. package/vendor/rbxutil/signal/init.luau +432 -432
  186. package/vendor/rbxutil/signal/init.test.luau +190 -190
  187. package/vendor/rbxutil/signal/package.json +17 -17
  188. package/vendor/rbxutil/signal/wally.toml +9 -9
  189. package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
  190. package/vendor/rbxutil/silo/Util.luau +55 -55
  191. package/vendor/rbxutil/silo/init.luau +338 -338
  192. package/vendor/rbxutil/silo/init.test.luau +215 -215
  193. package/vendor/rbxutil/silo/wally.toml +8 -8
  194. package/vendor/rbxutil/spring/index.d.ts +40 -40
  195. package/vendor/rbxutil/spring/init.luau +97 -97
  196. package/vendor/rbxutil/spring/package.json +17 -17
  197. package/vendor/rbxutil/spring/wally.toml +8 -8
  198. package/vendor/rbxutil/stream/index.d.ts +88 -88
  199. package/vendor/rbxutil/stream/init.luau +597 -597
  200. package/vendor/rbxutil/stream/package.json +18 -18
  201. package/vendor/rbxutil/stream/wally.toml +9 -9
  202. package/vendor/rbxutil/streamable/Streamable.luau +202 -202
  203. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
  204. package/vendor/rbxutil/streamable/init.luau +8 -8
  205. package/vendor/rbxutil/streamable/wally.toml +12 -12
  206. package/vendor/rbxutil/symbol/init.luau +56 -56
  207. package/vendor/rbxutil/symbol/init.test.luau +37 -37
  208. package/vendor/rbxutil/symbol/wally.toml +8 -8
  209. package/vendor/rbxutil/table-util/init.luau +938 -938
  210. package/vendor/rbxutil/table-util/init.test.luau +439 -439
  211. package/vendor/rbxutil/task-queue/index.d.ts +27 -27
  212. package/vendor/rbxutil/task-queue/init.luau +97 -97
  213. package/vendor/rbxutil/task-queue/wally.toml +8 -8
  214. package/vendor/rbxutil/timer/index.d.ts +81 -81
  215. package/vendor/rbxutil/timer/init.luau +249 -249
  216. package/vendor/rbxutil/timer/init.test.luau +73 -73
  217. package/vendor/rbxutil/timer/wally.toml +11 -11
  218. package/vendor/rbxutil/tree/index.d.ts +15 -15
  219. package/vendor/rbxutil/tree/init.luau +137 -137
  220. package/vendor/rbxutil/tree/wally.toml +8 -8
  221. package/vendor/rbxutil/trove/index.d.ts +46 -46
  222. package/vendor/rbxutil/trove/init.luau +787 -787
  223. package/vendor/rbxutil/trove/init.test.luau +203 -203
  224. package/vendor/rbxutil/trove/wally.toml +8 -8
  225. package/vendor/rbxutil/typed-remote/init.luau +196 -196
  226. package/vendor/rbxutil/typed-remote/wally.toml +8 -8
  227. package/vendor/rbxutil/wait-for/index.d.ts +17 -17
  228. package/vendor/rbxutil/wait-for/init.luau +257 -257
  229. package/vendor/rbxutil/wait-for/init.test.luau +182 -182
  230. package/vendor/rbxutil/wait-for/wally.toml +11 -11
  231. package/vendor/t/t.lua +1350 -1350
  232. package/vendor/testez/Context.lua +26 -26
  233. package/vendor/testez/Expectation.lua +311 -311
  234. package/vendor/testez/ExpectationContext.lua +38 -38
  235. package/vendor/testez/LifecycleHooks.lua +89 -89
  236. package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
  237. package/vendor/testez/Reporters/TextReporter.lua +105 -105
  238. package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
  239. package/vendor/testez/TestBootstrap.lua +146 -146
  240. package/vendor/testez/TestEnum.lua +27 -27
  241. package/vendor/testez/TestPlan.lua +304 -304
  242. package/vendor/testez/TestPlanner.lua +39 -39
  243. package/vendor/testez/TestResults.lua +111 -111
  244. package/vendor/testez/TestRunner.lua +188 -188
  245. package/vendor/testez/TestSession.lua +243 -243
  246. package/vendor/testez/init.lua +39 -39
@@ -1,1103 +1,1103 @@
1
- ---
2
- name: roblox-gui
3
- description: GUI systems, layout, responsiveness, cross-platform UI. ScreenGuis, UIListLayout, constraint-based design.
4
- last_reviewed: 2026-05-22
5
- ---
6
-
7
- <!-- Source: brockmartin/roblox-game-skill (MIT) -->
8
-
9
- # Roblox GUI/UI Systems Reference
10
-
11
- ## 1. Overview
12
-
13
- Load this reference when working on any UI-related task in Roblox:
14
-
15
- - Building menus (main menu, pause menu, settings)
16
- - HUDs (health bars, minimaps, ammo counters, score displays)
17
- - Shops and inventory screens
18
- - Notification and toast systems
19
- - Dialog and popup windows
20
- - Any 2D or 3D-attached interface elements
21
-
22
- 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.
23
-
24
- ---
25
-
26
- ## Quick Reference
27
-
28
- **Load Full Reference below only when you need specific layout examples or implementation patterns.**
29
-
30
- Key rules:
31
- - Mobile-first: design for phone, scale up. Touch targets minimum 48x48px.
32
- - Scale (0-1 proportional) for position/size. Offset only for fixed padding/icons.
33
- - Container Frame Rule: every logical group gets a Frame with layout modifier inside.
34
- - UIListLayout/UIGridLayout: set on parent Frame, children auto-arrange. AutomaticSize on parent.
35
- - ScreenGui.ResetOnSpawn = false for persistent UI. IgnoreGuiInset = true for fullscreen.
36
- - ZIndex for layering within same ScreenGui. DisplayOrder for ScreenGui priority.
37
- - Never use absolute pixel sizes for main containers. UISizeConstraint for min/max bounds.
38
- - ScrollingFrame: set CanvasSize or AutomaticCanvasSize. UIListLayout inside for content.
39
- - Common AI mistake: forgetting to set LayoutOrder on children when using layout modifiers.
40
- - For complex stateful UI (shops, inventories, settings), see the `roblox-gui-fusion` skill.
41
-
42
- ---
43
-
44
- ## Full Reference
45
-
46
- ## Design Guidelines
47
-
48
- <!-- Guidelines sourced from Roblox DevForum, official docs, and community standards -->
49
-
50
- ### Container Frame Rule
51
- <!-- Source: epochzx, Roblox DevForum -->
52
-
53
- **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.
54
-
55
- ```luau
56
- local container = Instance.new("Frame")
57
- container.Name = "Container"
58
- container.Size = UDim2.new(1, 0, 1, 0)
59
- container.BackgroundTransparency = 1
60
- container.BorderSizePixel = 0
61
- container.Parent = screenGui
62
- -- All UI children go under container, not directly under screenGui
63
- ```
64
-
65
- ### Scale vs Offset
66
- <!-- Source: uiuxartist (Roblox Staff), DevForum -->
67
-
68
- - **Scale** = percentage of parent (responsive). Use for Size and Position.
69
- - **Offset** = fixed pixels. Use for pixel-perfect icons, small graphics, UIStroke.
70
- - **UIStroke** does NOT support Scale - only Offset.
71
- - **UICorner** DOES support Scale (`UDim.new(0.5, 0)` = 50% radius).
72
- - **Hybrid pattern**: start pure Scale, add Offset for minimum size, reduce Scale.
73
-
74
- ```luau
75
- -- Scale for responsive sizing
76
- frame.Size = UDim2.new(0.5, 0, 0, 40) -- 50% width, 40px height
77
-
78
- -- Offset for UIStroke (Scale not supported)
79
- stroke.Thickness = 2 -- always Offset
80
-
81
- -- UICorner supports Scale
82
- corner.CornerRadius = UDim.new(0, 8) -- Offset
83
- corner.CornerRadius = UDim.new(0.5, 0) -- Scale (50% of smallest axis)
84
- ```
85
-
86
- ### Mobile-First Principles
87
- <!-- Source: Roblox official docs - Adaptive Design Guidelines -->
88
- <!-- https://create.roblox.com/docs/production/publishing/adaptive-design -->
89
-
90
- - **50%+ of Roblox players are on mobile.** Design touch-first.
91
- - Minimum touch target: **~0.15 width scale** (≈44-48px).
92
- - Account for notches via `ScreenGui.ScreenInsets`.
93
- - Don't place UI in the top 58px (Roblox top bar) or bottom virtual controls.
94
- - **Test with Device Emulator** before publishing (View → Device Emulator).
95
-
96
- ### Typography
97
- <!-- Source: PictureFolder, Roblox DevForum (119 likes) -->
98
- <!-- "Designing UI - Tips and Best Practices" -->
99
-
100
- - **Two fonts max**: Display (headers/buttons) + Body (descriptions).
101
- - **Gotham** is the de facto modern Roblox font.
102
- - **NEVER** pure white (`#FFFFFF`) on pure black (`#000000`). Use off-white (`#F0F0F0`) on dark gray (`#1E1E1E`).
103
- - **Size hierarchy**: bigger/bolder = more important.
104
-
105
- ### Color
106
- <!-- Source: DevForum "Modern UI Colour Schemes" -->
107
-
108
- - Dark palette: `20,20,20` (Modern Black) to `35,35,35` (Very Light). Don't go lighter than 35.
109
- - Grey base + accent colors for interactive elements.
110
- - **Pick a palette and stick to it.** Consistency > variety.
111
-
112
- ### Common AI UI Mistakes
113
-
114
- | Mistake | Fix |
115
- |---------|-----|
116
- | Elements directly under ScreenGui | Use a Container Frame child |
117
- | Pure Scale only | Too small on mobile - add Offset minimums |
118
- | Pure Offset only | Breaks on different resolutions |
119
- | Pure white on pure black text | Use off-white on dark gray |
120
- | Ignoring mobile players | 50%+ are mobile; design touch-first |
121
- | Text-heavy UI for young audiences | Use icons, images, minimal text |
122
- | UI overlapping top bar / chat / leaderboard | Respect safe areas (top 58px, bottom 100px) |
123
- | Not testing with Device Emulator | Always test before publishing |
124
-
125
- ---
126
-
127
- ## 2. GUI Hierarchy
128
-
129
- ### ScreenGui (2D Overlay)
130
-
131
- The primary container for all 2D UI. Placed in `StarterGui`; Roblox copies it into each player's `PlayerGui` on spawn.
132
-
133
- ```luau
134
- local Players = game:GetService("Players")
135
- local player = Players.LocalPlayer
136
- local playerGui = player:WaitForChild("PlayerGui")
137
-
138
- local screenGui = Instance.new("ScreenGui")
139
- screenGui.Name = "MainHUD"
140
- screenGui.ResetOnSpawn = false -- survives respawn
141
- screenGui.DisplayOrder = 10 -- higher renders on top
142
- screenGui.IgnoreGuiInset = true -- extends behind top bar
143
- screenGui.Parent = playerGui
144
- ```
145
-
146
- **Key Properties:**
147
-
148
- | Property | Purpose |
149
- |---|---|
150
- | `DisplayOrder` | Controls layering. Higher values render on top of lower values. |
151
- | `ResetOnSpawn` | `true` (default): destroyed and re-cloned on respawn. Set to `false` for persistent UI (shops, settings). |
152
- | `IgnoreGuiInset` | `true`: UI extends behind the top bar (CoreGui area). Use for fullscreen overlays. |
153
- | `Enabled` | Toggle visibility without destroying. |
154
-
155
- ### SurfaceGui (On Part Surfaces)
156
-
157
- Renders UI on a Part's surface. Used for in-world signs, screens, control panels.
158
-
159
- ```luau
160
- local surfaceGui = Instance.new("SurfaceGui")
161
- surfaceGui.Face = Enum.NormalId.Front
162
- surfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
163
- surfaceGui.PixelsPerStud = 50
164
- surfaceGui.Parent = workspace.SignPart
165
- -- Also set surfaceGui.Adornee = workspace.SignPart if parented elsewhere
166
- ```
167
-
168
- ### BillboardGui (Floating in 3D)
169
-
170
- Always faces the camera. Used for nametags, damage numbers, quest markers.
171
-
172
- ```luau
173
- local billboardGui = Instance.new("BillboardGui")
174
- billboardGui.Size = UDim2.new(0, 200, 0, 50)
175
- billboardGui.StudsOffset = Vector3.new(0, 3, 0) -- above the part
176
- billboardGui.AlwaysOnTop = false -- occluded by 3D geometry
177
- billboardGui.MaxDistance = 100 -- hides beyond this range
178
- billboardGui.Adornee = workspace.NPC.Head
179
- billboardGui.Parent = workspace.NPC.Head
180
- ```
181
-
182
- ### Display Order Hierarchy
183
-
184
- ```
185
- DisplayOrder 100 -- Modal dialogs (on top of everything)
186
- DisplayOrder 50 -- Notifications / toasts
187
- DisplayOrder 10 -- HUD elements
188
- DisplayOrder 1 -- Background UI
189
- ```
190
-
191
- ---
192
-
193
- ## 3. Core UI Elements
194
-
195
- ### Frame
196
-
197
- Container for grouping and styling. No text or image by default.
198
-
199
- ```luau
200
- local frame = Instance.new("Frame")
201
- frame.Size = UDim2.new(0.3, 0, 0.4, 0) -- 30% width, 40% height
202
- frame.Position = UDim2.new(0.5, 0, 0.5, 0) -- centered (with AnchorPoint)
203
- frame.AnchorPoint = Vector2.new(0.5, 0.5)
204
- frame.BackgroundColor3 = Color3.fromRGB(30, 30, 40)
205
- frame.BackgroundTransparency = 0.1
206
- frame.BorderSizePixel = 0
207
- frame.Parent = screenGui
208
- ```
209
-
210
- ### TextLabel / TextButton
211
-
212
- ```luau
213
- local label = Instance.new("TextLabel")
214
- label.Size = UDim2.new(1, 0, 0, 40)
215
- label.Text = "Score: 0"
216
- label.TextColor3 = Color3.fromRGB(255, 255, 255)
217
- label.TextScaled = true
218
- label.Font = Enum.Font.GothamBold
219
- label.BackgroundTransparency = 1
220
- label.Parent = frame
221
-
222
- local button = Instance.new("TextButton")
223
- button.Size = UDim2.new(0.5, 0, 0, 50)
224
- button.Text = "Purchase"
225
- button.TextColor3 = Color3.fromRGB(255, 255, 255)
226
- button.BackgroundColor3 = Color3.fromRGB(0, 170, 80)
227
- button.Font = Enum.Font.GothamBold
228
- button.TextSize = 18
229
- button.Parent = frame
230
-
231
- button.Activated:Connect(function()
232
- -- Activated works for mouse click, touch tap, and gamepad
233
- end)
234
- ```
235
-
236
- > Use `Activated` instead of `MouseButton1Click` for cross-platform support.
237
-
238
- ### ImageLabel / ImageButton
239
-
240
- ```luau
241
- local icon = Instance.new("ImageLabel")
242
- icon.Size = UDim2.new(0, 64, 0, 64)
243
- icon.Image = "rbxassetid://123456789"
244
- icon.ScaleType = Enum.ScaleType.Fit
245
- icon.BackgroundTransparency = 1
246
- icon.Parent = frame
247
- ```
248
-
249
- ### ScrollingFrame
250
-
251
- See **section 14 (ScrollingFrame Patterns)** for full coverage including AutomaticCanvasSize, UIListLayout integration, and elastic overscroll.
252
-
253
- ### ViewportFrame
254
-
255
- Renders 3D content inside a 2D GUI (item previews, character displays).
256
-
257
- ```luau
258
- local viewport = Instance.new("ViewportFrame")
259
- viewport.Size = UDim2.new(0, 200, 0, 200)
260
- viewport.BackgroundColor3 = Color3.fromRGB(20, 20, 20)
261
- viewport.Parent = frame
262
-
263
- -- Clone a model into the viewport
264
- local previewModel = workspace.SwordModel:Clone()
265
- previewModel.Parent = viewport
266
-
267
- -- Add a camera
268
- local camera = Instance.new("Camera")
269
- camera.CFrame = CFrame.new(Vector3.new(0, 2, 5), Vector3.new(0, 1, 0))
270
- camera.Parent = viewport
271
- viewport.CurrentCamera = camera
272
- ```
273
-
274
- ### Layout Modifiers
275
-
276
- | Modifier | Purpose |
277
- |---|---|
278
- | `UIListLayout` | Arranges children in a vertical or horizontal list |
279
- | `UIGridLayout` | Arranges children in a grid |
280
- | `UIPageLayout` | Swipeable pages (one child visible at a time) |
281
- | `UIPadding` | Inner padding on a container |
282
- | `UICorner` | Rounded corners |
283
- | `UIStroke` | Outline/border effect |
284
- | `UIGradient` | Color gradient on an element |
285
- | `UISizeConstraint` | Min/max pixel size |
286
- | `UIAspectRatioConstraint` | Locks width/height ratio |
287
-
288
- ```luau
289
- -- Rounded corners
290
- local corner = Instance.new("UICorner")
291
- corner.CornerRadius = UDim.new(0, 8)
292
- corner.Parent = frame
293
-
294
- -- Stroke/border
295
- local stroke = Instance.new("UIStroke")
296
- stroke.Color = Color3.fromRGB(255, 255, 255)
297
- stroke.Thickness = 2
298
- stroke.Transparency = 0.5
299
- stroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
300
- stroke.Parent = frame
301
-
302
- -- Gradient
303
- local gradient = Instance.new("UIGradient")
304
- gradient.Color = ColorSequence.new(
305
- Color3.fromRGB(50, 50, 80),
306
- Color3.fromRGB(20, 20, 40)
307
- )
308
- gradient.Rotation = 90
309
- gradient.Parent = frame
310
-
311
- -- Padding
312
- local padding = Instance.new("UIPadding")
313
- padding.PaddingLeft = UDim.new(0, 12)
314
- padding.PaddingRight = UDim.new(0, 12)
315
- padding.PaddingTop = UDim.new(0, 12)
316
- padding.PaddingBottom = UDim.new(0, 12)
317
- padding.Parent = frame
318
- ```
319
-
320
- ---
321
-
322
- ## 4. Layout Systems
323
-
324
- ### UIListLayout
325
-
326
- Arranges children sequentially. Best for menus, sidebars, chat messages, vertical/horizontal lists.
327
-
328
- ```luau
329
- local listLayout = Instance.new("UIListLayout")
330
- listLayout.FillDirection = Enum.FillDirection.Vertical
331
- listLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
332
- listLayout.VerticalAlignment = Enum.VerticalAlignment.Top
333
- listLayout.SortOrder = Enum.SortOrder.LayoutOrder
334
- listLayout.Padding = UDim.new(0, 8) -- gap between items
335
- listLayout.Parent = frame
336
- ```
337
-
338
- Children are sorted by their `LayoutOrder` property (lower values first).
339
-
340
- ### UIGridLayout
341
-
342
- Arranges children in rows and columns. Best for inventories, shops, icon grids.
343
-
344
- ```luau
345
- local gridLayout = Instance.new("UIGridLayout")
346
- gridLayout.CellSize = UDim2.new(0, 80, 0, 80)
347
- gridLayout.CellPadding = UDim2.new(0, 8, 0, 8)
348
- gridLayout.FillDirection = Enum.FillDirection.Horizontal
349
- gridLayout.FillDirectionMaxCells = 4 -- 4 columns, then wrap
350
- gridLayout.SortOrder = Enum.SortOrder.LayoutOrder
351
- gridLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
352
- gridLayout.Parent = scrollFrame
353
- ```
354
-
355
- ### UIPageLayout
356
-
357
- Shows one child at a time with animated page transitions. Best for tabbed menus, tutorials, onboarding flows.
358
-
359
- ```luau
360
- local pageLayout = Instance.new("UIPageLayout")
361
- pageLayout.Animated = true
362
- pageLayout.EasingStyle = Enum.EasingStyle.Quad
363
- pageLayout.EasingDirection = Enum.EasingDirection.InOut
364
- pageLayout.TweenTime = 0.3
365
- pageLayout.Circular = false -- cannot loop from last to first
366
- pageLayout.Padding = UDim.new(0, 0)
367
- pageLayout.Parent = frame
368
-
369
- -- Navigate pages
370
- pageLayout:JumpToIndex(0)
371
- pageLayout:Next()
372
- pageLayout:Previous()
373
- pageLayout:JumpTo(someChildFrame)
374
- ```
375
-
376
- ### When to Use Each
377
-
378
- | Scenario | Layout |
379
- |---|---|
380
- | Vertical menu, chat log, leaderboard | `UIListLayout` |
381
- | Inventory grid, shop items, emoji picker | `UIGridLayout` |
382
- | Settings tabs, tutorial slides, shop categories | `UIPageLayout` |
383
- | HUD element pinned to a corner | Absolute positioning (no layout) |
384
- | Overlapping elements (health bar segments) | Absolute positioning |
385
-
386
- ### Absolute vs Layout-Driven
387
-
388
- - **Absolute positioning**: Set `Position` and `Size` directly. Use for HUD elements pinned to specific screen locations.
389
- - **Layout-driven**: Add a layout object as a child. The layout overrides children's `Position`. Use for dynamic lists where content count changes.
390
-
391
- You can nest both: a frame with absolute position containing children managed by a UIListLayout.
392
-
393
- ---
394
-
395
- ## 5. Responsive Design
396
-
397
- Scale vs Offset rules are in **Design Guidelines** above. This section covers constraints and adaptive patterns.
398
-
399
- ### UISizeConstraint
400
-
401
- Prevents elements from becoming too small on phones or too large on ultrawide monitors.
402
-
403
- ```luau
404
- local sizeConstraint = Instance.new("UISizeConstraint")
405
- sizeConstraint.MinSize = Vector2.new(200, 150)
406
- sizeConstraint.MaxSize = Vector2.new(600, 450)
407
- sizeConstraint.Parent = frame
408
- ```
409
-
410
- ### UIAspectRatioConstraint
411
-
412
- Locks an element's aspect ratio so it does not stretch.
413
-
414
- ```luau
415
- local aspect = Instance.new("UIAspectRatioConstraint")
416
- aspect.AspectRatio = 16 / 9
417
- aspect.AspectType = Enum.AspectType.FitWithinMaxSize
418
- aspect.DominantAxis = Enum.DominantAxis.Width
419
- aspect.Parent = frame
420
- ```
421
-
422
- ### Adapting to Screen Size
423
-
424
- ```luau
425
- local camera = workspace.CurrentCamera
426
-
427
- local function adaptUI()
428
- local viewportSize = camera.ViewportSize
429
- local isPortrait = viewportSize.Y > viewportSize.X
430
- local isSmallScreen = viewportSize.X < 600
431
-
432
- if isSmallScreen then
433
- frame.Size = UDim2.new(0.95, 0, 0.8, 0) -- nearly fullscreen on mobile
434
- elseif isPortrait then
435
- frame.Size = UDim2.new(0.7, 0, 0.5, 0)
436
- else
437
- frame.Size = UDim2.new(0.3, 0, 0.4, 0) -- standard desktop
438
- end
439
- end
440
-
441
- camera:GetPropertyChangedSignal("ViewportSize"):Connect(adaptUI)
442
- adaptUI()
443
- ```
444
-
445
- ---
446
-
447
- ## 6. Animation with TweenService
448
-
449
- ### TweenInfo
450
-
451
- ```luau
452
- local TweenService = game:GetService("TweenService")
453
-
454
- -- TweenInfo.new(time, easingStyle, easingDirection, repeatCount, reverses, delayTime)
455
- local tweenInfo = TweenInfo.new(
456
- 0.5, -- duration in seconds
457
- Enum.EasingStyle.Quad, -- easing curve
458
- Enum.EasingDirection.Out, -- direction
459
- 0, -- repeat count (0 = no repeat, -1 = infinite)
460
- false, -- reverses
461
- 0 -- delay before starting
462
- )
463
- ```
464
-
465
- **Common Easing Styles:**
466
-
467
- | Style | Use Case |
468
- |---|---|
469
- | `Quad` | General-purpose, smooth |
470
- | `Back` | Slight overshoot, bouncy buttons |
471
- | `Elastic` | Springy, attention-grabbing |
472
- | `Bounce` | Bouncing effect at the end |
473
- | `Linear` | Constant speed, progress bars |
474
- | `Sine` | Gentle, subtle motion |
475
- | `Exponential` | Dramatic acceleration/deceleration |
476
-
477
- ### Tweening UI Properties
478
-
479
- ```luau
480
- -- Slide in from the right
481
- frame.Position = UDim2.new(1.5, 0, 0.5, 0)
482
-
483
- local slideIn = TweenService:Create(frame, TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
484
- Position = UDim2.new(0.5, 0, 0.5, 0),
485
- })
486
- slideIn:Play()
487
-
488
- -- Fade in
489
- frame.BackgroundTransparency = 1
490
- local fadeIn = TweenService:Create(frame, TweenInfo.new(0.3), {
491
- BackgroundTransparency = 0,
492
- })
493
- fadeIn:Play()
494
-
495
- -- Color transition
496
- local colorShift = TweenService:Create(frame, TweenInfo.new(0.5), {
497
- BackgroundColor3 = Color3.fromRGB(255, 50, 50),
498
- })
499
- colorShift:Play()
500
-
501
- -- Size pulse
502
- local pulse = TweenService:Create(button, TweenInfo.new(0.6, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, true), {
503
- Size = UDim2.new(0.55, 0, 0, 55),
504
- })
505
- pulse:Play()
506
- ```
507
-
508
- ### Chaining Tweens
509
-
510
- ```luau
511
- local step1 = TweenService:Create(frame, TweenInfo.new(0.3), {
512
- Position = UDim2.new(0.5, 0, 0.5, 0),
513
- })
514
- local step2 = TweenService:Create(frame, TweenInfo.new(0.2), {
515
- Size = UDim2.new(0.4, 0, 0.5, 0),
516
- })
517
-
518
- step1.Completed:Connect(function()
519
- step2:Play()
520
- end)
521
- step1:Play()
522
- ```
523
-
524
- ### Common UI Animation Recipes
525
-
526
- ```luau
527
- -- Bounce entrance
528
- local function bounceIn(element: GuiObject)
529
- element.Size = UDim2.new(0, 0, 0, 0)
530
- element.AnchorPoint = Vector2.new(0.5, 0.5)
531
- local tween = TweenService:Create(element, TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
532
- Size = UDim2.new(0.3, 0, 0.4, 0),
533
- })
534
- tween:Play()
535
- return tween
536
- end
537
-
538
- -- Fade + scale dismiss
539
- local function dismiss(element: GuiObject): RBXScriptSignal
540
- local tween = TweenService:Create(element, TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
541
- Size = UDim2.new(0, 0, 0, 0),
542
- BackgroundTransparency = 1,
543
- })
544
- tween:Play()
545
- return tween.Completed
546
- end
547
- ```
548
-
549
- ---
550
-
551
- ## 7. Input Handling
552
-
553
- ### UserInputService
554
-
555
- Low-level input detection. Best for keyboard shortcuts, mouse tracking, detecting input type.
556
-
557
- ```luau
558
- local UserInputService = game:GetService("UserInputService")
559
-
560
- -- Keyboard input
561
- UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessed: boolean)
562
- if gameProcessed then return end -- ignore if typing in a TextBox, etc.
563
-
564
- if input.KeyCode == Enum.KeyCode.E then
565
- toggleInventory()
566
- elseif input.KeyCode == Enum.KeyCode.Escape then
567
- togglePauseMenu()
568
- end
569
- end)
570
-
571
- -- Mouse position
572
- local mousePos = UserInputService:GetMouseLocation() -- Vector2
573
-
574
- -- Detect platform
575
- local isMobile = UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
576
- local isConsole = UserInputService.GamepadEnabled
577
-
578
- -- Hide mouse cursor
579
- UserInputService.MouseIconEnabled = false
580
- ```
581
-
582
- ### ContextActionService
583
-
584
- Higher-level action binding. Automatically generates mobile buttons. Best for game actions (interact, reload, sprint).
585
-
586
- ```luau
587
- local ContextActionService = game:GetService("ContextActionService")
588
-
589
- local function onInteract(actionName: string, inputState: Enum.UserInputState, inputObject: InputObject)
590
- if inputState == Enum.UserInputState.Begin then
591
- interactWithNearestObject()
592
- end
593
- return Enum.ContextActionResult.Sink -- consume the input
594
- end
595
-
596
- -- Bind to E key, touch button auto-created on mobile
597
- ContextActionService:BindAction("Interact", onInteract, true, Enum.KeyCode.E)
598
-
599
- -- Customize the mobile button
600
- ContextActionService:SetPosition("Interact", UDim2.new(0.8, 0, 0.5, 0))
601
- ContextActionService:SetTitle("Interact", "E")
602
- ContextActionService:SetImage("Interact", "rbxassetid://123456789")
603
-
604
- -- Unbind when no longer needed
605
- ContextActionService:UnbindAction("Interact")
606
- ```
607
-
608
- ### When to Use Each
609
-
610
- | Service | Best For |
611
- |---|---|
612
- | `UserInputService` | Global hotkeys, mouse tracking, detecting input device type, custom cursor |
613
- | `ContextActionService` | In-game actions that need mobile buttons, context-sensitive controls (e.g., "interact" only near objects) |
614
- | `GuiButton.Activated` | UI button clicks (already cross-platform) |
615
-
616
- ---
617
-
618
- ## 8. Common UI Patterns (Skeletons)
619
-
620
- ### Shop Interface
621
-
622
- **Problem:** Display purchasable items in a grid with hover feedback and server-validated purchase.
623
-
624
- **Structure:**
625
- - ScrollingFrame + UIGridLayout (container)
626
- - Frame per item (card)
627
- - ImageLabel (icon)
628
- - TextLabel (name + price)
629
- - TextButton (buy) with hover tween
630
-
631
- **Key properties:** UIGridLayout.CellSize for responsive cards, UICorner for rounded edges, TweenService for hover color shift. Purchase via RemoteFunction (server validates, returns success/fail).
632
-
633
- ### Health Bar
634
-
635
- **Problem:** Show player health with color thresholds and damage trail effect.
636
-
637
- **Structure:**
638
- - Frame (container, dark background)
639
- - Frame (damage trail, red, tweens to match fill with delay)
640
- - Frame (fill, green→yellow→red based on %)
641
-
642
- **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.
643
-
644
- ### Notification Toast
645
-
646
- **Problem:** Temporary message that slides in, stays briefly, slides out.
647
-
648
- **Structure:**
649
- - Frame (anchored off-screen right)
650
- - TextLabel (message)
651
- - UICorner + UIStroke
652
-
653
- **Key logic:** Tween Position from off-screen to visible, task.delay(3), tween back out, Destroy. Queue multiple toasts with vertical offset.
654
-
655
- ### Dialog / Popup System
656
-
657
- **Problem:** Modal dialog with backdrop, title, body, confirm/cancel buttons.
658
-
659
- **Structure:**
660
- - Frame (backdrop, full-screen, semi-transparent black)
661
- - Frame (dialog card, centered, 0.4x0.3 scale)
662
- - TextLabel (title)
663
- - TextLabel (body)
664
- - Frame (button row)
665
- - TextButton (confirm)
666
- - TextButton (cancel)
667
-
668
- **Key logic:** Show/hide by toggling Visible + tween BackgroundTransparency on backdrop. Return result via Promise or callback. Destroy on close.
669
-
670
- ---
671
- ## 9. Best Practices
672
-
673
- ### Positioning and Sizing
674
-
675
- - **Always use Scale** for `Position` and `Size` to support all screen resolutions.
676
- - **Use Offset** only for small fixed-pixel elements (icon sizes, padding, border thickness).
677
- - **Always set AnchorPoint** when centering elements: `AnchorPoint = Vector2.new(0.5, 0.5)`.
678
- - **Use UISizeConstraint** to set minimum and maximum sizes so UI does not become unusable on small screens or absurdly large on ultrawide monitors.
679
-
680
- ### Style and Theme Consistency
681
-
682
- - Define colors, fonts, and sizes as constants at the top of your module.
683
- - Create a UI style/theme module that all scripts reference.
684
-
685
- ```luau
686
- -- UITheme.luau (ModuleScript in ReplicatedStorage)
687
- local Theme = {
688
- Colors = {
689
- Background = Color3.fromRGB(25, 25, 35),
690
- Surface = Color3.fromRGB(40, 40, 55),
691
- Primary = Color3.fromRGB(0, 150, 70),
692
- Danger = Color3.fromRGB(200, 40, 40),
693
- TextPrimary = Color3.fromRGB(255, 255, 255),
694
- TextSecondary = Color3.fromRGB(180, 180, 190),
695
- Accent = Color3.fromRGB(255, 215, 0),
696
- },
697
- Fonts = {
698
- Title = Enum.Font.GothamBold,
699
- Body = Enum.Font.Gotham,
700
- Mono = Enum.Font.RobotoMono,
701
- },
702
- TextSizes = {
703
- Title = 24,
704
- Subtitle = 18,
705
- Body = 14,
706
- Small = 12,
707
- },
708
- CornerRadius = UDim.new(0, 8),
709
- Padding = UDim.new(0, 12),
710
- }
711
-
712
- return Theme
713
- ```
714
-
715
- ### Accessibility
716
-
717
- - Minimum text size of **14pt** for body text; 12pt only for tertiary labels.
718
- - Maintain **high contrast** between text and background (white on dark, dark on light).
719
- - Use `TextScaled = true` with `TextWrapped = true` sparingly; prefer explicit `TextSize` for control.
720
- - Provide visual feedback on all interactive elements (hover color change, press scale).
721
- - Support gamepad navigation with `GuiService:Select()` for console players.
722
-
723
- ### Performance: GUI Pooling
724
-
725
- For scrolling lists with many items (inventory, leaderboard), reuse GUI elements instead of creating/destroying them.
726
-
727
- ```luau
728
- local pool: { Frame } = {}
729
-
730
- local function getCard(): Frame
731
- local card = table.remove(pool)
732
- if not card then
733
- card = createNewCard() -- only create if pool is empty
734
- end
735
- card.Visible = true
736
- return card
737
- end
738
-
739
- local function returnCard(card: Frame)
740
- card.Visible = false
741
- card.Parent = nil
742
- table.insert(pool, card)
743
- end
744
- ```
745
-
746
- ### General
747
-
748
- - Set `ScreenGui.Enabled = false` when a UI is not visible rather than destroying and recreating it.
749
- - Use `Activated` on buttons instead of `MouseButton1Click` for cross-platform support.
750
- - Disconnect event connections when UI is destroyed to prevent memory leaks.
751
- - Keep UI logic in ModuleScripts; keep LocalScripts thin (just wiring).
752
- - Always validate purchases on the server. The client UI is only for display.
753
-
754
- ---
755
-
756
- ## 10. Anti-Patterns
757
-
758
- ### Hardcoded pixel positions
759
-
760
- ```luau
761
- -- BAD: breaks on different resolutions
762
- frame.Position = UDim2.new(0, 500, 0, 300)
763
- frame.Size = UDim2.new(0, 400, 0, 200)
764
-
765
- -- GOOD: responsive
766
- frame.Position = UDim2.new(0.5, 0, 0.5, 0)
767
- frame.Size = UDim2.new(0.3, 0, 0.25, 0)
768
- frame.AnchorPoint = Vector2.new(0.5, 0.5)
769
- ```
770
-
771
- ### Creating new GUIs every frame
772
-
773
- ```luau
774
- -- BAD: creates garbage every frame, causes lag
775
- RunService.RenderStepped:Connect(function()
776
- local label = Instance.new("TextLabel")
777
- label.Text = `Score: ${score}`
778
- label.Parent = screenGui
779
- end)
780
-
781
- -- GOOD: update existing element
782
- RunService.RenderStepped:Connect(function()
783
- scoreLabel.Text = `Score: ${score}`
784
- end)
785
- ```
786
-
787
- ### Not cleaning up event connections
788
-
789
- ```luau
790
- -- BAD: connection persists after UI is removed, leaks memory
791
- button.Activated:Connect(function()
792
- doSomething()
793
- end)
794
-
795
- -- GOOD: store and disconnect
796
- local connection = button.Activated:Connect(function()
797
- doSomething()
798
- end)
799
-
800
- -- When done:
801
- connection:Disconnect()
802
-
803
- -- OR use :Once() for single-fire events
804
- button.Activated:Once(function()
805
- doSomething()
806
- end)
807
- ```
808
-
809
- ### Blocking the UI thread with yields
810
-
811
- ```luau
812
- -- BAD: freezes the entire UI
813
- button.Activated:Connect(function()
814
- task.wait(5) -- nothing else can happen for 5 seconds
815
- label.Text = "Done"
816
- end)
817
-
818
- -- GOOD: use task.delay or task.spawn for async work
819
- button.Activated:Connect(function()
820
- button.Active = false
821
- task.spawn(function()
822
- -- do async work
823
- task.wait(5)
824
- label.Text = "Done"
825
- button.Active = true
826
- end)
827
- end)
828
- ```
829
-
830
- ### Trusting client UI for game logic
831
-
832
- ```luau
833
- -- BAD: client decides if purchase succeeds
834
- button.Activated:Connect(function()
835
- coins -= item.price -- client-side deduction, exploitable
836
- end)
837
-
838
- -- GOOD: server validates everything
839
- button.Activated:Connect(function()
840
- local success = purchaseRemote:InvokeServer(itemId)
841
- if success then
842
- updateCoinsDisplay()
843
- end
844
- end)
845
- ```
846
-
847
- ### Ignoring mobile and gamepad
848
-
849
- ```luau
850
- -- BAD: only works with mouse
851
- button.MouseButton1Click:Connect(handler)
852
-
853
- -- GOOD: works on mouse, touch, and gamepad
854
- button.Activated:Connect(handler)
855
- ```
856
-
857
- ---
858
-
859
- ## 11. Mobile-First Design
860
-
861
- 78% of Roblox traffic is mobile. Design for touch first, adapt for desktop/gamepad.
862
-
863
- ### Touch Targets
864
-
865
- - Minimum touch target: **44×44 px** (Apple guideline) or **48×48 dp** (Material Design)
866
- - Buttons smaller than 44px are frustrating on phones
867
- - Scale buttons 1.4× on touch devices:
868
-
869
- ```luau
870
- local UserInputService = game:GetService("UserInputService")
871
-
872
- local function isTouchDevice(): boolean
873
- return UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
874
- end
875
-
876
- -- Apply at UI creation time
877
- if isTouchDevice() then
878
- button.Size = UDim2.new(button.Size.X.Scale * 1.4, 0, button.Size.Y.Scale * 1.4, 0)
879
- end
880
- ```
881
-
882
- ### PreferredInput API (2025+)
883
-
884
- Modern approach to input detection. Use instead of checking individual booleans:
885
-
886
- ```luau
887
- local preferred = UserInputService.PreferredInput
888
- -- Enum.PreferredInput values:
889
- -- Touch, MouseAndKeyboard, Gamepad, None
890
-
891
- if preferred == Enum.PreferredInput.Touch then
892
- -- enlarge buttons, simplify layout
893
- elseif preferred == Enum.PreferredInput.Gamepad then
894
- -- highlight focused element, add navigation hints
895
- end
896
-
897
- UserInputService:GetPropertyChangedSignal("PreferredInput"):Connect(function()
898
- -- re-adapt UI when input method changes
899
- end)
900
- ```
901
-
902
- ### Safe Areas and Screen Real Estate
903
-
904
- - Top bar takes **58px**. Use `ScreenGui.IgnoreGuiInset = true` for fullscreen, but don't put critical content behind it.
905
- - Bottom of screen has virtual controls on mobile. Keep interactive elements above the bottom 100px.
906
- - Use the **Device Emulator** in Studio (View → Device Emulator) to test phone/tablet views before shipping.
907
-
908
- ### Mobile Layout Rules
909
-
910
- - Prefer vertical layouts over horizontal on mobile (scrolling down is natural)
911
- - Avoid sidebars - use bottom sheets or full-screen overlays instead
912
- - Use `UIScale` to zoom entire UI proportionally on small screens:
913
-
914
- ```luau
915
- local uiScale = Instance.new("UIScale")
916
- uiScale.Scale = math.min(1, camera.ViewportSize.X / 1080) -- reference width
917
- uiScale.Parent = screenGui
918
- ```
919
-
920
- ### Gamepad Navigation
921
-
922
- For console players:
923
-
924
- ```luau
925
- local GuiService = game:GetService("GuiService")
926
-
927
- -- Set the initially selected object when opening a menu
928
- GuiService.SelectedObject = myButton
929
-
930
- -- On menu close, clear selection
931
- GuiService.SelectedObject = nil
932
- ```
933
-
934
- Source: Roblox Adaptive Design Guidelines (create.roblox.com/docs/production/publishing/adaptive-design)
935
-
936
- ---
937
-
938
- ## 12. Text Handling
939
-
940
- ### AutomaticSize vs TextScaled
941
-
942
- **Do NOT use `TextScaled = true`.** It makes text unreadably small on mobile and truncates text that doesn't fit.
943
-
944
- Use `AutomaticSize` instead. It resizes the UI element to fit the text at a consistent, readable font size:
945
-
946
- ```luau
947
- -- BAD: text scales to fit, becomes unreadable on small screens
948
- label.TextScaled = true
949
-
950
- -- GOOD: label grows/shrinks to fit text at fixed readable size
951
- label.TextSize = 16
952
- label.TextWrapped = true
953
- label.AutomaticSize = Enum.AutomaticSize.Y -- height adjusts to content
954
- ```
955
-
956
- ### UITextSizeConstraint
957
-
958
- If you must use `TextScaled`, always add a constraint:
959
-
960
- ```luau
961
- local textSizeConstraint = Instance.new("UITextSizeConstraint")
962
- textSizeConstraint.MaxTextSize = 24
963
- textSizeConstraint.MinTextSize = 12 -- NEVER below 9 (official hard rule)
964
- textSizeConstraint.Parent = label
965
- ```
966
-
967
- ### Text Truncation
968
-
969
- ```luau
970
- label.TextTruncate = Enum.TextTruncate.AtEnd -- shows "..." when text overflows
971
- label.TextWrapped = true -- wrap instead of truncate for multi-line content
972
- ```
973
-
974
- ### Calculating Text Size Programmatically
975
-
976
- ```luau
977
- local TextService = game:GetService("TextService")
978
-
979
- local bounds = TextService:GetTextSize(
980
- "Hello World",
981
- 16, -- TextSize
982
- Enum.Font.Gotham,
983
- Vector2.new(200, math.huge) -- max width, unlimited height
984
- )
985
- -- bounds.X = actual width, bounds.Y = actual height
986
- ```
987
-
988
- Source: Roblox Size Modifiers & Constraints docs (create.roblox.com/docs/ui/size-modifiers)
989
-
990
- ---
991
-
992
- ## 13. UI Styling (StyleEditor)
993
-
994
- Roblox ships a CSS-like styling system (2025+). Use it for consistent theming.
995
-
996
- ### Style Tokens
997
-
998
- Named values (like CSS variables). Define once, reference everywhere.
999
-
1000
- ### Style Rules with Selectors
1001
-
1002
- ```luau
1003
- -- Style rules can target by class, tag, name, state, modifier, query
1004
- -- Similar to CSS selectors
1005
- ```
1006
-
1007
- ### When to Use StyleEditor vs Code Themes
1008
-
1009
- | Approach | Best For |
1010
- |----------|----------|
1011
- | Studio StyleEditor | Static UI built visually, team collaboration on design |
1012
- | Code-based UITheme module | Dynamic UI built in code, programmatic theming |
1013
-
1014
- Both can coexist. StyleEditor is the "build in Studio" answer; code themes are the "build in code" answer.
1015
-
1016
- Source: Roblox UI Styling docs (create.roblox.com/docs/ui/styling)
1017
-
1018
- ---
1019
-
1020
- ## 14. ScrollingFrame Patterns
1021
-
1022
- ### AutomaticCanvasSize (Use This)
1023
-
1024
- ```luau
1025
- -- BAD: manually calculating canvas size
1026
- scrollFrame.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y)
1027
-
1028
- -- GOOD: engine handles it automatically
1029
- scrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
1030
- scrollFrame.ScrollingDirection = Enum.ScrollingDirection.Y
1031
- ```
1032
-
1033
- ### ScrollingFrame + UIListLayout
1034
-
1035
- The most common pattern - a scrollable list:
1036
-
1037
- ```luau
1038
- local scrollFrame = Instance.new("ScrollingFrame")
1039
- scrollFrame.Size = UDim2.new(1, 0, 1, 0)
1040
- scrollFrame.BackgroundTransparency = 1
1041
- scrollFrame.ScrollBarThickness = 6
1042
- scrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
1043
- scrollFrame.ScrollingDirection = Enum.ScrollingDirection.Y
1044
- scrollFrame.Parent = container
1045
-
1046
- local listLayout = Instance.new("UIListLayout")
1047
- listLayout.FillDirection = Enum.FillDirection.Vertical
1048
- listLayout.Padding = UDim.new(0, 8)
1049
- listLayout.SortOrder = Enum.SortOrder.LayoutOrder
1050
- listLayout.Parent = scrollFrame
1051
-
1052
- -- Add items as children of scrollFrame
1053
- -- They auto-arrange vertically, scrollFrame auto-sizes
1054
- ```
1055
-
1056
- ### ScrollingFrame + UIGridLayout
1057
-
1058
- For scrollable grids (inventory, shop):
1059
-
1060
- ```luau
1061
- local scrollFrame = Instance.new("ScrollingFrame")
1062
- scrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
1063
- scrollFrame.ScrollingDirection = Enum.ScrollingDirection.Y
1064
- scrollFrame.Parent = container
1065
-
1066
- local gridLayout = Instance.new("UIGridLayout")
1067
- gridLayout.CellSize = UDim2.new(0, 80, 0, 80)
1068
- gridLayout.CellPadding = UDim2.new(0, 8, 0, 8)
1069
- gridLayout.FillDirectionMaxCells = 4 -- 4 columns
1070
- gridLayout.SortOrder = Enum.SortOrder.LayoutOrder
1071
- gridLayout.Parent = scrollFrame
1072
- ```
1073
-
1074
- ### Elastic Overscroll
1075
-
1076
- The bouncy effect on iOS/Android. Enabled by default. Disable if unwanted:
1077
-
1078
- ```luau
1079
- scrollFrame.ElasticBehavior = Enum.ElasticBehavior.Never
1080
- ```
1081
-
1082
- Source: Roblox Scrolling Frames docs (create.roblox.com/docs/ui/scrolling-frames)
1083
-
1084
- ---
1085
-
1086
- ## Sources
1087
-
1088
- - Roblox Adaptive Design Guidelines: create.roblox.com/docs/production/publishing/adaptive-design
1089
- - Roblox Size Modifiers & Constraints: create.roblox.com/docs/ui/size-modifiers
1090
- - Roblox UI Styling: create.roblox.com/docs/ui/styling
1091
- - Roblox Scrolling Frames: create.roblox.com/docs/ui/scrolling-frames
1092
- - Roblox List & Flex Layouts: create.roblox.com/docs/ui/list-flex-layouts
1093
- - Roblox Grid & Table Layouts: create.roblox.com/docs/ui/grid-table-layouts
1094
- - DevForum: Designing UI - Tips and Best Practices (Roblox Staff)
1095
- - DevForum: Design Mobile First
1096
- - DevForum: GUI Optimization Tips
1097
- - DevForum: epochzx - Container Frame Rule for ScreenGui
1098
- - DevForum: uiuxartist (Roblox Staff) - Scale vs Offset guidelines
1099
- - DevForum: PictureFolder - UI Design Tips and Best Practices (119 likes)
1100
- - DevForum: Modern UI Colour Schemes - dark palette guidelines
1101
- - Fusion: github.com/dphfox/Fusion (MIT)
1102
- - Vide: github.com/centau/vide (MIT)
1103
- - brockmartin/roblox-game-skill (MIT) - base content
1
+ ---
2
+ name: roblox-gui
3
+ description: GUI systems, layout, responsiveness, cross-platform UI. ScreenGuis, UIListLayout, constraint-based design.
4
+ last_reviewed: 2026-05-22
5
+ ---
6
+
7
+ <!-- Source: brockmartin/roblox-game-skill (MIT) -->
8
+
9
+ # Roblox GUI/UI Systems Reference
10
+
11
+ ## 1. Overview
12
+
13
+ Load this reference when working on any UI-related task in Roblox:
14
+
15
+ - Building menus (main menu, pause menu, settings)
16
+ - HUDs (health bars, minimaps, ammo counters, score displays)
17
+ - Shops and inventory screens
18
+ - Notification and toast systems
19
+ - Dialog and popup windows
20
+ - Any 2D or 3D-attached interface elements
21
+
22
+ 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.
23
+
24
+ ---
25
+
26
+ ## Quick Reference
27
+
28
+ **Load Full Reference below only when you need specific layout examples or implementation patterns.**
29
+
30
+ Key rules:
31
+ - Mobile-first: design for phone, scale up. Touch targets minimum 48x48px.
32
+ - Scale (0-1 proportional) for position/size. Offset only for fixed padding/icons.
33
+ - Container Frame Rule: every logical group gets a Frame with layout modifier inside.
34
+ - UIListLayout/UIGridLayout: set on parent Frame, children auto-arrange. AutomaticSize on parent.
35
+ - ScreenGui.ResetOnSpawn = false for persistent UI. IgnoreGuiInset = true for fullscreen.
36
+ - ZIndex for layering within same ScreenGui. DisplayOrder for ScreenGui priority.
37
+ - Never use absolute pixel sizes for main containers. UISizeConstraint for min/max bounds.
38
+ - ScrollingFrame: set CanvasSize or AutomaticCanvasSize. UIListLayout inside for content.
39
+ - Common AI mistake: forgetting to set LayoutOrder on children when using layout modifiers.
40
+ - For complex stateful UI (shops, inventories, settings), see the `roblox-gui-fusion` skill.
41
+
42
+ ---
43
+
44
+ ## Full Reference
45
+
46
+ ## Design Guidelines
47
+
48
+ <!-- Guidelines sourced from Roblox DevForum, official docs, and community standards -->
49
+
50
+ ### Container Frame Rule
51
+ <!-- Source: epochzx, Roblox DevForum -->
52
+
53
+ **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.
54
+
55
+ ```luau
56
+ local container = Instance.new("Frame")
57
+ container.Name = "Container"
58
+ container.Size = UDim2.new(1, 0, 1, 0)
59
+ container.BackgroundTransparency = 1
60
+ container.BorderSizePixel = 0
61
+ container.Parent = screenGui
62
+ -- All UI children go under container, not directly under screenGui
63
+ ```
64
+
65
+ ### Scale vs Offset
66
+ <!-- Source: uiuxartist (Roblox Staff), DevForum -->
67
+
68
+ - **Scale** = percentage of parent (responsive). Use for Size and Position.
69
+ - **Offset** = fixed pixels. Use for pixel-perfect icons, small graphics, UIStroke.
70
+ - **UIStroke** does NOT support Scale - only Offset.
71
+ - **UICorner** DOES support Scale (`UDim.new(0.5, 0)` = 50% radius).
72
+ - **Hybrid pattern**: start pure Scale, add Offset for minimum size, reduce Scale.
73
+
74
+ ```luau
75
+ -- Scale for responsive sizing
76
+ frame.Size = UDim2.new(0.5, 0, 0, 40) -- 50% width, 40px height
77
+
78
+ -- Offset for UIStroke (Scale not supported)
79
+ stroke.Thickness = 2 -- always Offset
80
+
81
+ -- UICorner supports Scale
82
+ corner.CornerRadius = UDim.new(0, 8) -- Offset
83
+ corner.CornerRadius = UDim.new(0.5, 0) -- Scale (50% of smallest axis)
84
+ ```
85
+
86
+ ### Mobile-First Principles
87
+ <!-- Source: Roblox official docs - Adaptive Design Guidelines -->
88
+ <!-- https://create.roblox.com/docs/production/publishing/adaptive-design -->
89
+
90
+ - **50%+ of Roblox players are on mobile.** Design touch-first.
91
+ - Minimum touch target: **~0.15 width scale** (≈44-48px).
92
+ - Account for notches via `ScreenGui.ScreenInsets`.
93
+ - Don't place UI in the top 58px (Roblox top bar) or bottom virtual controls.
94
+ - **Test with Device Emulator** before publishing (View → Device Emulator).
95
+
96
+ ### Typography
97
+ <!-- Source: PictureFolder, Roblox DevForum (119 likes) -->
98
+ <!-- "Designing UI - Tips and Best Practices" -->
99
+
100
+ - **Two fonts max**: Display (headers/buttons) + Body (descriptions).
101
+ - **Gotham** is the de facto modern Roblox font.
102
+ - **NEVER** pure white (`#FFFFFF`) on pure black (`#000000`). Use off-white (`#F0F0F0`) on dark gray (`#1E1E1E`).
103
+ - **Size hierarchy**: bigger/bolder = more important.
104
+
105
+ ### Color
106
+ <!-- Source: DevForum "Modern UI Colour Schemes" -->
107
+
108
+ - Dark palette: `20,20,20` (Modern Black) to `35,35,35` (Very Light). Don't go lighter than 35.
109
+ - Grey base + accent colors for interactive elements.
110
+ - **Pick a palette and stick to it.** Consistency > variety.
111
+
112
+ ### Common AI UI Mistakes
113
+
114
+ | Mistake | Fix |
115
+ |---------|-----|
116
+ | Elements directly under ScreenGui | Use a Container Frame child |
117
+ | Pure Scale only | Too small on mobile - add Offset minimums |
118
+ | Pure Offset only | Breaks on different resolutions |
119
+ | Pure white on pure black text | Use off-white on dark gray |
120
+ | Ignoring mobile players | 50%+ are mobile; design touch-first |
121
+ | Text-heavy UI for young audiences | Use icons, images, minimal text |
122
+ | UI overlapping top bar / chat / leaderboard | Respect safe areas (top 58px, bottom 100px) |
123
+ | Not testing with Device Emulator | Always test before publishing |
124
+
125
+ ---
126
+
127
+ ## 2. GUI Hierarchy
128
+
129
+ ### ScreenGui (2D Overlay)
130
+
131
+ The primary container for all 2D UI. Placed in `StarterGui`; Roblox copies it into each player's `PlayerGui` on spawn.
132
+
133
+ ```luau
134
+ local Players = game:GetService("Players")
135
+ local player = Players.LocalPlayer
136
+ local playerGui = player:WaitForChild("PlayerGui")
137
+
138
+ local screenGui = Instance.new("ScreenGui")
139
+ screenGui.Name = "MainHUD"
140
+ screenGui.ResetOnSpawn = false -- survives respawn
141
+ screenGui.DisplayOrder = 10 -- higher renders on top
142
+ screenGui.IgnoreGuiInset = true -- extends behind top bar
143
+ screenGui.Parent = playerGui
144
+ ```
145
+
146
+ **Key Properties:**
147
+
148
+ | Property | Purpose |
149
+ |---|---|
150
+ | `DisplayOrder` | Controls layering. Higher values render on top of lower values. |
151
+ | `ResetOnSpawn` | `true` (default): destroyed and re-cloned on respawn. Set to `false` for persistent UI (shops, settings). |
152
+ | `IgnoreGuiInset` | `true`: UI extends behind the top bar (CoreGui area). Use for fullscreen overlays. |
153
+ | `Enabled` | Toggle visibility without destroying. |
154
+
155
+ ### SurfaceGui (On Part Surfaces)
156
+
157
+ Renders UI on a Part's surface. Used for in-world signs, screens, control panels.
158
+
159
+ ```luau
160
+ local surfaceGui = Instance.new("SurfaceGui")
161
+ surfaceGui.Face = Enum.NormalId.Front
162
+ surfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
163
+ surfaceGui.PixelsPerStud = 50
164
+ surfaceGui.Parent = workspace.SignPart
165
+ -- Also set surfaceGui.Adornee = workspace.SignPart if parented elsewhere
166
+ ```
167
+
168
+ ### BillboardGui (Floating in 3D)
169
+
170
+ Always faces the camera. Used for nametags, damage numbers, quest markers.
171
+
172
+ ```luau
173
+ local billboardGui = Instance.new("BillboardGui")
174
+ billboardGui.Size = UDim2.new(0, 200, 0, 50)
175
+ billboardGui.StudsOffset = Vector3.new(0, 3, 0) -- above the part
176
+ billboardGui.AlwaysOnTop = false -- occluded by 3D geometry
177
+ billboardGui.MaxDistance = 100 -- hides beyond this range
178
+ billboardGui.Adornee = workspace.NPC.Head
179
+ billboardGui.Parent = workspace.NPC.Head
180
+ ```
181
+
182
+ ### Display Order Hierarchy
183
+
184
+ ```
185
+ DisplayOrder 100 -- Modal dialogs (on top of everything)
186
+ DisplayOrder 50 -- Notifications / toasts
187
+ DisplayOrder 10 -- HUD elements
188
+ DisplayOrder 1 -- Background UI
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 3. Core UI Elements
194
+
195
+ ### Frame
196
+
197
+ Container for grouping and styling. No text or image by default.
198
+
199
+ ```luau
200
+ local frame = Instance.new("Frame")
201
+ frame.Size = UDim2.new(0.3, 0, 0.4, 0) -- 30% width, 40% height
202
+ frame.Position = UDim2.new(0.5, 0, 0.5, 0) -- centered (with AnchorPoint)
203
+ frame.AnchorPoint = Vector2.new(0.5, 0.5)
204
+ frame.BackgroundColor3 = Color3.fromRGB(30, 30, 40)
205
+ frame.BackgroundTransparency = 0.1
206
+ frame.BorderSizePixel = 0
207
+ frame.Parent = screenGui
208
+ ```
209
+
210
+ ### TextLabel / TextButton
211
+
212
+ ```luau
213
+ local label = Instance.new("TextLabel")
214
+ label.Size = UDim2.new(1, 0, 0, 40)
215
+ label.Text = "Score: 0"
216
+ label.TextColor3 = Color3.fromRGB(255, 255, 255)
217
+ label.TextScaled = true
218
+ label.Font = Enum.Font.GothamBold
219
+ label.BackgroundTransparency = 1
220
+ label.Parent = frame
221
+
222
+ local button = Instance.new("TextButton")
223
+ button.Size = UDim2.new(0.5, 0, 0, 50)
224
+ button.Text = "Purchase"
225
+ button.TextColor3 = Color3.fromRGB(255, 255, 255)
226
+ button.BackgroundColor3 = Color3.fromRGB(0, 170, 80)
227
+ button.Font = Enum.Font.GothamBold
228
+ button.TextSize = 18
229
+ button.Parent = frame
230
+
231
+ button.Activated:Connect(function()
232
+ -- Activated works for mouse click, touch tap, and gamepad
233
+ end)
234
+ ```
235
+
236
+ > Use `Activated` instead of `MouseButton1Click` for cross-platform support.
237
+
238
+ ### ImageLabel / ImageButton
239
+
240
+ ```luau
241
+ local icon = Instance.new("ImageLabel")
242
+ icon.Size = UDim2.new(0, 64, 0, 64)
243
+ icon.Image = "rbxassetid://123456789"
244
+ icon.ScaleType = Enum.ScaleType.Fit
245
+ icon.BackgroundTransparency = 1
246
+ icon.Parent = frame
247
+ ```
248
+
249
+ ### ScrollingFrame
250
+
251
+ See **section 14 (ScrollingFrame Patterns)** for full coverage including AutomaticCanvasSize, UIListLayout integration, and elastic overscroll.
252
+
253
+ ### ViewportFrame
254
+
255
+ Renders 3D content inside a 2D GUI (item previews, character displays).
256
+
257
+ ```luau
258
+ local viewport = Instance.new("ViewportFrame")
259
+ viewport.Size = UDim2.new(0, 200, 0, 200)
260
+ viewport.BackgroundColor3 = Color3.fromRGB(20, 20, 20)
261
+ viewport.Parent = frame
262
+
263
+ -- Clone a model into the viewport
264
+ local previewModel = workspace.SwordModel:Clone()
265
+ previewModel.Parent = viewport
266
+
267
+ -- Add a camera
268
+ local camera = Instance.new("Camera")
269
+ camera.CFrame = CFrame.new(Vector3.new(0, 2, 5), Vector3.new(0, 1, 0))
270
+ camera.Parent = viewport
271
+ viewport.CurrentCamera = camera
272
+ ```
273
+
274
+ ### Layout Modifiers
275
+
276
+ | Modifier | Purpose |
277
+ |---|---|
278
+ | `UIListLayout` | Arranges children in a vertical or horizontal list |
279
+ | `UIGridLayout` | Arranges children in a grid |
280
+ | `UIPageLayout` | Swipeable pages (one child visible at a time) |
281
+ | `UIPadding` | Inner padding on a container |
282
+ | `UICorner` | Rounded corners |
283
+ | `UIStroke` | Outline/border effect |
284
+ | `UIGradient` | Color gradient on an element |
285
+ | `UISizeConstraint` | Min/max pixel size |
286
+ | `UIAspectRatioConstraint` | Locks width/height ratio |
287
+
288
+ ```luau
289
+ -- Rounded corners
290
+ local corner = Instance.new("UICorner")
291
+ corner.CornerRadius = UDim.new(0, 8)
292
+ corner.Parent = frame
293
+
294
+ -- Stroke/border
295
+ local stroke = Instance.new("UIStroke")
296
+ stroke.Color = Color3.fromRGB(255, 255, 255)
297
+ stroke.Thickness = 2
298
+ stroke.Transparency = 0.5
299
+ stroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
300
+ stroke.Parent = frame
301
+
302
+ -- Gradient
303
+ local gradient = Instance.new("UIGradient")
304
+ gradient.Color = ColorSequence.new(
305
+ Color3.fromRGB(50, 50, 80),
306
+ Color3.fromRGB(20, 20, 40)
307
+ )
308
+ gradient.Rotation = 90
309
+ gradient.Parent = frame
310
+
311
+ -- Padding
312
+ local padding = Instance.new("UIPadding")
313
+ padding.PaddingLeft = UDim.new(0, 12)
314
+ padding.PaddingRight = UDim.new(0, 12)
315
+ padding.PaddingTop = UDim.new(0, 12)
316
+ padding.PaddingBottom = UDim.new(0, 12)
317
+ padding.Parent = frame
318
+ ```
319
+
320
+ ---
321
+
322
+ ## 4. Layout Systems
323
+
324
+ ### UIListLayout
325
+
326
+ Arranges children sequentially. Best for menus, sidebars, chat messages, vertical/horizontal lists.
327
+
328
+ ```luau
329
+ local listLayout = Instance.new("UIListLayout")
330
+ listLayout.FillDirection = Enum.FillDirection.Vertical
331
+ listLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
332
+ listLayout.VerticalAlignment = Enum.VerticalAlignment.Top
333
+ listLayout.SortOrder = Enum.SortOrder.LayoutOrder
334
+ listLayout.Padding = UDim.new(0, 8) -- gap between items
335
+ listLayout.Parent = frame
336
+ ```
337
+
338
+ Children are sorted by their `LayoutOrder` property (lower values first).
339
+
340
+ ### UIGridLayout
341
+
342
+ Arranges children in rows and columns. Best for inventories, shops, icon grids.
343
+
344
+ ```luau
345
+ local gridLayout = Instance.new("UIGridLayout")
346
+ gridLayout.CellSize = UDim2.new(0, 80, 0, 80)
347
+ gridLayout.CellPadding = UDim2.new(0, 8, 0, 8)
348
+ gridLayout.FillDirection = Enum.FillDirection.Horizontal
349
+ gridLayout.FillDirectionMaxCells = 4 -- 4 columns, then wrap
350
+ gridLayout.SortOrder = Enum.SortOrder.LayoutOrder
351
+ gridLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
352
+ gridLayout.Parent = scrollFrame
353
+ ```
354
+
355
+ ### UIPageLayout
356
+
357
+ Shows one child at a time with animated page transitions. Best for tabbed menus, tutorials, onboarding flows.
358
+
359
+ ```luau
360
+ local pageLayout = Instance.new("UIPageLayout")
361
+ pageLayout.Animated = true
362
+ pageLayout.EasingStyle = Enum.EasingStyle.Quad
363
+ pageLayout.EasingDirection = Enum.EasingDirection.InOut
364
+ pageLayout.TweenTime = 0.3
365
+ pageLayout.Circular = false -- cannot loop from last to first
366
+ pageLayout.Padding = UDim.new(0, 0)
367
+ pageLayout.Parent = frame
368
+
369
+ -- Navigate pages
370
+ pageLayout:JumpToIndex(0)
371
+ pageLayout:Next()
372
+ pageLayout:Previous()
373
+ pageLayout:JumpTo(someChildFrame)
374
+ ```
375
+
376
+ ### When to Use Each
377
+
378
+ | Scenario | Layout |
379
+ |---|---|
380
+ | Vertical menu, chat log, leaderboard | `UIListLayout` |
381
+ | Inventory grid, shop items, emoji picker | `UIGridLayout` |
382
+ | Settings tabs, tutorial slides, shop categories | `UIPageLayout` |
383
+ | HUD element pinned to a corner | Absolute positioning (no layout) |
384
+ | Overlapping elements (health bar segments) | Absolute positioning |
385
+
386
+ ### Absolute vs Layout-Driven
387
+
388
+ - **Absolute positioning**: Set `Position` and `Size` directly. Use for HUD elements pinned to specific screen locations.
389
+ - **Layout-driven**: Add a layout object as a child. The layout overrides children's `Position`. Use for dynamic lists where content count changes.
390
+
391
+ You can nest both: a frame with absolute position containing children managed by a UIListLayout.
392
+
393
+ ---
394
+
395
+ ## 5. Responsive Design
396
+
397
+ Scale vs Offset rules are in **Design Guidelines** above. This section covers constraints and adaptive patterns.
398
+
399
+ ### UISizeConstraint
400
+
401
+ Prevents elements from becoming too small on phones or too large on ultrawide monitors.
402
+
403
+ ```luau
404
+ local sizeConstraint = Instance.new("UISizeConstraint")
405
+ sizeConstraint.MinSize = Vector2.new(200, 150)
406
+ sizeConstraint.MaxSize = Vector2.new(600, 450)
407
+ sizeConstraint.Parent = frame
408
+ ```
409
+
410
+ ### UIAspectRatioConstraint
411
+
412
+ Locks an element's aspect ratio so it does not stretch.
413
+
414
+ ```luau
415
+ local aspect = Instance.new("UIAspectRatioConstraint")
416
+ aspect.AspectRatio = 16 / 9
417
+ aspect.AspectType = Enum.AspectType.FitWithinMaxSize
418
+ aspect.DominantAxis = Enum.DominantAxis.Width
419
+ aspect.Parent = frame
420
+ ```
421
+
422
+ ### Adapting to Screen Size
423
+
424
+ ```luau
425
+ local camera = workspace.CurrentCamera
426
+
427
+ local function adaptUI()
428
+ local viewportSize = camera.ViewportSize
429
+ local isPortrait = viewportSize.Y > viewportSize.X
430
+ local isSmallScreen = viewportSize.X < 600
431
+
432
+ if isSmallScreen then
433
+ frame.Size = UDim2.new(0.95, 0, 0.8, 0) -- nearly fullscreen on mobile
434
+ elseif isPortrait then
435
+ frame.Size = UDim2.new(0.7, 0, 0.5, 0)
436
+ else
437
+ frame.Size = UDim2.new(0.3, 0, 0.4, 0) -- standard desktop
438
+ end
439
+ end
440
+
441
+ camera:GetPropertyChangedSignal("ViewportSize"):Connect(adaptUI)
442
+ adaptUI()
443
+ ```
444
+
445
+ ---
446
+
447
+ ## 6. Animation with TweenService
448
+
449
+ ### TweenInfo
450
+
451
+ ```luau
452
+ local TweenService = game:GetService("TweenService")
453
+
454
+ -- TweenInfo.new(time, easingStyle, easingDirection, repeatCount, reverses, delayTime)
455
+ local tweenInfo = TweenInfo.new(
456
+ 0.5, -- duration in seconds
457
+ Enum.EasingStyle.Quad, -- easing curve
458
+ Enum.EasingDirection.Out, -- direction
459
+ 0, -- repeat count (0 = no repeat, -1 = infinite)
460
+ false, -- reverses
461
+ 0 -- delay before starting
462
+ )
463
+ ```
464
+
465
+ **Common Easing Styles:**
466
+
467
+ | Style | Use Case |
468
+ |---|---|
469
+ | `Quad` | General-purpose, smooth |
470
+ | `Back` | Slight overshoot, bouncy buttons |
471
+ | `Elastic` | Springy, attention-grabbing |
472
+ | `Bounce` | Bouncing effect at the end |
473
+ | `Linear` | Constant speed, progress bars |
474
+ | `Sine` | Gentle, subtle motion |
475
+ | `Exponential` | Dramatic acceleration/deceleration |
476
+
477
+ ### Tweening UI Properties
478
+
479
+ ```luau
480
+ -- Slide in from the right
481
+ frame.Position = UDim2.new(1.5, 0, 0.5, 0)
482
+
483
+ local slideIn = TweenService:Create(frame, TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
484
+ Position = UDim2.new(0.5, 0, 0.5, 0),
485
+ })
486
+ slideIn:Play()
487
+
488
+ -- Fade in
489
+ frame.BackgroundTransparency = 1
490
+ local fadeIn = TweenService:Create(frame, TweenInfo.new(0.3), {
491
+ BackgroundTransparency = 0,
492
+ })
493
+ fadeIn:Play()
494
+
495
+ -- Color transition
496
+ local colorShift = TweenService:Create(frame, TweenInfo.new(0.5), {
497
+ BackgroundColor3 = Color3.fromRGB(255, 50, 50),
498
+ })
499
+ colorShift:Play()
500
+
501
+ -- Size pulse
502
+ local pulse = TweenService:Create(button, TweenInfo.new(0.6, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, true), {
503
+ Size = UDim2.new(0.55, 0, 0, 55),
504
+ })
505
+ pulse:Play()
506
+ ```
507
+
508
+ ### Chaining Tweens
509
+
510
+ ```luau
511
+ local step1 = TweenService:Create(frame, TweenInfo.new(0.3), {
512
+ Position = UDim2.new(0.5, 0, 0.5, 0),
513
+ })
514
+ local step2 = TweenService:Create(frame, TweenInfo.new(0.2), {
515
+ Size = UDim2.new(0.4, 0, 0.5, 0),
516
+ })
517
+
518
+ step1.Completed:Connect(function()
519
+ step2:Play()
520
+ end)
521
+ step1:Play()
522
+ ```
523
+
524
+ ### Common UI Animation Recipes
525
+
526
+ ```luau
527
+ -- Bounce entrance
528
+ local function bounceIn(element: GuiObject)
529
+ element.Size = UDim2.new(0, 0, 0, 0)
530
+ element.AnchorPoint = Vector2.new(0.5, 0.5)
531
+ local tween = TweenService:Create(element, TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
532
+ Size = UDim2.new(0.3, 0, 0.4, 0),
533
+ })
534
+ tween:Play()
535
+ return tween
536
+ end
537
+
538
+ -- Fade + scale dismiss
539
+ local function dismiss(element: GuiObject): RBXScriptSignal
540
+ local tween = TweenService:Create(element, TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
541
+ Size = UDim2.new(0, 0, 0, 0),
542
+ BackgroundTransparency = 1,
543
+ })
544
+ tween:Play()
545
+ return tween.Completed
546
+ end
547
+ ```
548
+
549
+ ---
550
+
551
+ ## 7. Input Handling
552
+
553
+ ### UserInputService
554
+
555
+ Low-level input detection. Best for keyboard shortcuts, mouse tracking, detecting input type.
556
+
557
+ ```luau
558
+ local UserInputService = game:GetService("UserInputService")
559
+
560
+ -- Keyboard input
561
+ UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessed: boolean)
562
+ if gameProcessed then return end -- ignore if typing in a TextBox, etc.
563
+
564
+ if input.KeyCode == Enum.KeyCode.E then
565
+ toggleInventory()
566
+ elseif input.KeyCode == Enum.KeyCode.Escape then
567
+ togglePauseMenu()
568
+ end
569
+ end)
570
+
571
+ -- Mouse position
572
+ local mousePos = UserInputService:GetMouseLocation() -- Vector2
573
+
574
+ -- Detect platform
575
+ local isMobile = UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
576
+ local isConsole = UserInputService.GamepadEnabled
577
+
578
+ -- Hide mouse cursor
579
+ UserInputService.MouseIconEnabled = false
580
+ ```
581
+
582
+ ### ContextActionService
583
+
584
+ Higher-level action binding. Automatically generates mobile buttons. Best for game actions (interact, reload, sprint).
585
+
586
+ ```luau
587
+ local ContextActionService = game:GetService("ContextActionService")
588
+
589
+ local function onInteract(actionName: string, inputState: Enum.UserInputState, inputObject: InputObject)
590
+ if inputState == Enum.UserInputState.Begin then
591
+ interactWithNearestObject()
592
+ end
593
+ return Enum.ContextActionResult.Sink -- consume the input
594
+ end
595
+
596
+ -- Bind to E key, touch button auto-created on mobile
597
+ ContextActionService:BindAction("Interact", onInteract, true, Enum.KeyCode.E)
598
+
599
+ -- Customize the mobile button
600
+ ContextActionService:SetPosition("Interact", UDim2.new(0.8, 0, 0.5, 0))
601
+ ContextActionService:SetTitle("Interact", "E")
602
+ ContextActionService:SetImage("Interact", "rbxassetid://123456789")
603
+
604
+ -- Unbind when no longer needed
605
+ ContextActionService:UnbindAction("Interact")
606
+ ```
607
+
608
+ ### When to Use Each
609
+
610
+ | Service | Best For |
611
+ |---|---|
612
+ | `UserInputService` | Global hotkeys, mouse tracking, detecting input device type, custom cursor |
613
+ | `ContextActionService` | In-game actions that need mobile buttons, context-sensitive controls (e.g., "interact" only near objects) |
614
+ | `GuiButton.Activated` | UI button clicks (already cross-platform) |
615
+
616
+ ---
617
+
618
+ ## 8. Common UI Patterns (Skeletons)
619
+
620
+ ### Shop Interface
621
+
622
+ **Problem:** Display purchasable items in a grid with hover feedback and server-validated purchase.
623
+
624
+ **Structure:**
625
+ - ScrollingFrame + UIGridLayout (container)
626
+ - Frame per item (card)
627
+ - ImageLabel (icon)
628
+ - TextLabel (name + price)
629
+ - TextButton (buy) with hover tween
630
+
631
+ **Key properties:** UIGridLayout.CellSize for responsive cards, UICorner for rounded edges, TweenService for hover color shift. Purchase via RemoteFunction (server validates, returns success/fail).
632
+
633
+ ### Health Bar
634
+
635
+ **Problem:** Show player health with color thresholds and damage trail effect.
636
+
637
+ **Structure:**
638
+ - Frame (container, dark background)
639
+ - Frame (damage trail, red, tweens to match fill with delay)
640
+ - Frame (fill, green→yellow→red based on %)
641
+
642
+ **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.
643
+
644
+ ### Notification Toast
645
+
646
+ **Problem:** Temporary message that slides in, stays briefly, slides out.
647
+
648
+ **Structure:**
649
+ - Frame (anchored off-screen right)
650
+ - TextLabel (message)
651
+ - UICorner + UIStroke
652
+
653
+ **Key logic:** Tween Position from off-screen to visible, task.delay(3), tween back out, Destroy. Queue multiple toasts with vertical offset.
654
+
655
+ ### Dialog / Popup System
656
+
657
+ **Problem:** Modal dialog with backdrop, title, body, confirm/cancel buttons.
658
+
659
+ **Structure:**
660
+ - Frame (backdrop, full-screen, semi-transparent black)
661
+ - Frame (dialog card, centered, 0.4x0.3 scale)
662
+ - TextLabel (title)
663
+ - TextLabel (body)
664
+ - Frame (button row)
665
+ - TextButton (confirm)
666
+ - TextButton (cancel)
667
+
668
+ **Key logic:** Show/hide by toggling Visible + tween BackgroundTransparency on backdrop. Return result via Promise or callback. Destroy on close.
669
+
670
+ ---
671
+ ## 9. Best Practices
672
+
673
+ ### Positioning and Sizing
674
+
675
+ - **Always use Scale** for `Position` and `Size` to support all screen resolutions.
676
+ - **Use Offset** only for small fixed-pixel elements (icon sizes, padding, border thickness).
677
+ - **Always set AnchorPoint** when centering elements: `AnchorPoint = Vector2.new(0.5, 0.5)`.
678
+ - **Use UISizeConstraint** to set minimum and maximum sizes so UI does not become unusable on small screens or absurdly large on ultrawide monitors.
679
+
680
+ ### Style and Theme Consistency
681
+
682
+ - Define colors, fonts, and sizes as constants at the top of your module.
683
+ - Create a UI style/theme module that all scripts reference.
684
+
685
+ ```luau
686
+ -- UITheme.luau (ModuleScript in ReplicatedStorage)
687
+ local Theme = {
688
+ Colors = {
689
+ Background = Color3.fromRGB(25, 25, 35),
690
+ Surface = Color3.fromRGB(40, 40, 55),
691
+ Primary = Color3.fromRGB(0, 150, 70),
692
+ Danger = Color3.fromRGB(200, 40, 40),
693
+ TextPrimary = Color3.fromRGB(255, 255, 255),
694
+ TextSecondary = Color3.fromRGB(180, 180, 190),
695
+ Accent = Color3.fromRGB(255, 215, 0),
696
+ },
697
+ Fonts = {
698
+ Title = Enum.Font.GothamBold,
699
+ Body = Enum.Font.Gotham,
700
+ Mono = Enum.Font.RobotoMono,
701
+ },
702
+ TextSizes = {
703
+ Title = 24,
704
+ Subtitle = 18,
705
+ Body = 14,
706
+ Small = 12,
707
+ },
708
+ CornerRadius = UDim.new(0, 8),
709
+ Padding = UDim.new(0, 12),
710
+ }
711
+
712
+ return Theme
713
+ ```
714
+
715
+ ### Accessibility
716
+
717
+ - Minimum text size of **14pt** for body text; 12pt only for tertiary labels.
718
+ - Maintain **high contrast** between text and background (white on dark, dark on light).
719
+ - Use `TextScaled = true` with `TextWrapped = true` sparingly; prefer explicit `TextSize` for control.
720
+ - Provide visual feedback on all interactive elements (hover color change, press scale).
721
+ - Support gamepad navigation with `GuiService:Select()` for console players.
722
+
723
+ ### Performance: GUI Pooling
724
+
725
+ For scrolling lists with many items (inventory, leaderboard), reuse GUI elements instead of creating/destroying them.
726
+
727
+ ```luau
728
+ local pool: { Frame } = {}
729
+
730
+ local function getCard(): Frame
731
+ local card = table.remove(pool)
732
+ if not card then
733
+ card = createNewCard() -- only create if pool is empty
734
+ end
735
+ card.Visible = true
736
+ return card
737
+ end
738
+
739
+ local function returnCard(card: Frame)
740
+ card.Visible = false
741
+ card.Parent = nil
742
+ table.insert(pool, card)
743
+ end
744
+ ```
745
+
746
+ ### General
747
+
748
+ - Set `ScreenGui.Enabled = false` when a UI is not visible rather than destroying and recreating it.
749
+ - Use `Activated` on buttons instead of `MouseButton1Click` for cross-platform support.
750
+ - Disconnect event connections when UI is destroyed to prevent memory leaks.
751
+ - Keep UI logic in ModuleScripts; keep LocalScripts thin (just wiring).
752
+ - Always validate purchases on the server. The client UI is only for display.
753
+
754
+ ---
755
+
756
+ ## 10. Anti-Patterns
757
+
758
+ ### Hardcoded pixel positions
759
+
760
+ ```luau
761
+ -- BAD: breaks on different resolutions
762
+ frame.Position = UDim2.new(0, 500, 0, 300)
763
+ frame.Size = UDim2.new(0, 400, 0, 200)
764
+
765
+ -- GOOD: responsive
766
+ frame.Position = UDim2.new(0.5, 0, 0.5, 0)
767
+ frame.Size = UDim2.new(0.3, 0, 0.25, 0)
768
+ frame.AnchorPoint = Vector2.new(0.5, 0.5)
769
+ ```
770
+
771
+ ### Creating new GUIs every frame
772
+
773
+ ```luau
774
+ -- BAD: creates garbage every frame, causes lag
775
+ RunService.RenderStepped:Connect(function()
776
+ local label = Instance.new("TextLabel")
777
+ label.Text = `Score: ${score}`
778
+ label.Parent = screenGui
779
+ end)
780
+
781
+ -- GOOD: update existing element
782
+ RunService.RenderStepped:Connect(function()
783
+ scoreLabel.Text = `Score: ${score}`
784
+ end)
785
+ ```
786
+
787
+ ### Not cleaning up event connections
788
+
789
+ ```luau
790
+ -- BAD: connection persists after UI is removed, leaks memory
791
+ button.Activated:Connect(function()
792
+ doSomething()
793
+ end)
794
+
795
+ -- GOOD: store and disconnect
796
+ local connection = button.Activated:Connect(function()
797
+ doSomething()
798
+ end)
799
+
800
+ -- When done:
801
+ connection:Disconnect()
802
+
803
+ -- OR use :Once() for single-fire events
804
+ button.Activated:Once(function()
805
+ doSomething()
806
+ end)
807
+ ```
808
+
809
+ ### Blocking the UI thread with yields
810
+
811
+ ```luau
812
+ -- BAD: freezes the entire UI
813
+ button.Activated:Connect(function()
814
+ task.wait(5) -- nothing else can happen for 5 seconds
815
+ label.Text = "Done"
816
+ end)
817
+
818
+ -- GOOD: use task.delay or task.spawn for async work
819
+ button.Activated:Connect(function()
820
+ button.Active = false
821
+ task.spawn(function()
822
+ -- do async work
823
+ task.wait(5)
824
+ label.Text = "Done"
825
+ button.Active = true
826
+ end)
827
+ end)
828
+ ```
829
+
830
+ ### Trusting client UI for game logic
831
+
832
+ ```luau
833
+ -- BAD: client decides if purchase succeeds
834
+ button.Activated:Connect(function()
835
+ coins -= item.price -- client-side deduction, exploitable
836
+ end)
837
+
838
+ -- GOOD: server validates everything
839
+ button.Activated:Connect(function()
840
+ local success = purchaseRemote:InvokeServer(itemId)
841
+ if success then
842
+ updateCoinsDisplay()
843
+ end
844
+ end)
845
+ ```
846
+
847
+ ### Ignoring mobile and gamepad
848
+
849
+ ```luau
850
+ -- BAD: only works with mouse
851
+ button.MouseButton1Click:Connect(handler)
852
+
853
+ -- GOOD: works on mouse, touch, and gamepad
854
+ button.Activated:Connect(handler)
855
+ ```
856
+
857
+ ---
858
+
859
+ ## 11. Mobile-First Design
860
+
861
+ 78% of Roblox traffic is mobile. Design for touch first, adapt for desktop/gamepad.
862
+
863
+ ### Touch Targets
864
+
865
+ - Minimum touch target: **44×44 px** (Apple guideline) or **48×48 dp** (Material Design)
866
+ - Buttons smaller than 44px are frustrating on phones
867
+ - Scale buttons 1.4× on touch devices:
868
+
869
+ ```luau
870
+ local UserInputService = game:GetService("UserInputService")
871
+
872
+ local function isTouchDevice(): boolean
873
+ return UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
874
+ end
875
+
876
+ -- Apply at UI creation time
877
+ if isTouchDevice() then
878
+ button.Size = UDim2.new(button.Size.X.Scale * 1.4, 0, button.Size.Y.Scale * 1.4, 0)
879
+ end
880
+ ```
881
+
882
+ ### PreferredInput API (2025+)
883
+
884
+ Modern approach to input detection. Use instead of checking individual booleans:
885
+
886
+ ```luau
887
+ local preferred = UserInputService.PreferredInput
888
+ -- Enum.PreferredInput values:
889
+ -- Touch, MouseAndKeyboard, Gamepad, None
890
+
891
+ if preferred == Enum.PreferredInput.Touch then
892
+ -- enlarge buttons, simplify layout
893
+ elseif preferred == Enum.PreferredInput.Gamepad then
894
+ -- highlight focused element, add navigation hints
895
+ end
896
+
897
+ UserInputService:GetPropertyChangedSignal("PreferredInput"):Connect(function()
898
+ -- re-adapt UI when input method changes
899
+ end)
900
+ ```
901
+
902
+ ### Safe Areas and Screen Real Estate
903
+
904
+ - Top bar takes **58px**. Use `ScreenGui.IgnoreGuiInset = true` for fullscreen, but don't put critical content behind it.
905
+ - Bottom of screen has virtual controls on mobile. Keep interactive elements above the bottom 100px.
906
+ - Use the **Device Emulator** in Studio (View → Device Emulator) to test phone/tablet views before shipping.
907
+
908
+ ### Mobile Layout Rules
909
+
910
+ - Prefer vertical layouts over horizontal on mobile (scrolling down is natural)
911
+ - Avoid sidebars - use bottom sheets or full-screen overlays instead
912
+ - Use `UIScale` to zoom entire UI proportionally on small screens:
913
+
914
+ ```luau
915
+ local uiScale = Instance.new("UIScale")
916
+ uiScale.Scale = math.min(1, camera.ViewportSize.X / 1080) -- reference width
917
+ uiScale.Parent = screenGui
918
+ ```
919
+
920
+ ### Gamepad Navigation
921
+
922
+ For console players:
923
+
924
+ ```luau
925
+ local GuiService = game:GetService("GuiService")
926
+
927
+ -- Set the initially selected object when opening a menu
928
+ GuiService.SelectedObject = myButton
929
+
930
+ -- On menu close, clear selection
931
+ GuiService.SelectedObject = nil
932
+ ```
933
+
934
+ Source: Roblox Adaptive Design Guidelines (create.roblox.com/docs/production/publishing/adaptive-design)
935
+
936
+ ---
937
+
938
+ ## 12. Text Handling
939
+
940
+ ### AutomaticSize vs TextScaled
941
+
942
+ **Do NOT use `TextScaled = true`.** It makes text unreadably small on mobile and truncates text that doesn't fit.
943
+
944
+ Use `AutomaticSize` instead. It resizes the UI element to fit the text at a consistent, readable font size:
945
+
946
+ ```luau
947
+ -- BAD: text scales to fit, becomes unreadable on small screens
948
+ label.TextScaled = true
949
+
950
+ -- GOOD: label grows/shrinks to fit text at fixed readable size
951
+ label.TextSize = 16
952
+ label.TextWrapped = true
953
+ label.AutomaticSize = Enum.AutomaticSize.Y -- height adjusts to content
954
+ ```
955
+
956
+ ### UITextSizeConstraint
957
+
958
+ If you must use `TextScaled`, always add a constraint:
959
+
960
+ ```luau
961
+ local textSizeConstraint = Instance.new("UITextSizeConstraint")
962
+ textSizeConstraint.MaxTextSize = 24
963
+ textSizeConstraint.MinTextSize = 12 -- NEVER below 9 (official hard rule)
964
+ textSizeConstraint.Parent = label
965
+ ```
966
+
967
+ ### Text Truncation
968
+
969
+ ```luau
970
+ label.TextTruncate = Enum.TextTruncate.AtEnd -- shows "..." when text overflows
971
+ label.TextWrapped = true -- wrap instead of truncate for multi-line content
972
+ ```
973
+
974
+ ### Calculating Text Size Programmatically
975
+
976
+ ```luau
977
+ local TextService = game:GetService("TextService")
978
+
979
+ local bounds = TextService:GetTextSize(
980
+ "Hello World",
981
+ 16, -- TextSize
982
+ Enum.Font.Gotham,
983
+ Vector2.new(200, math.huge) -- max width, unlimited height
984
+ )
985
+ -- bounds.X = actual width, bounds.Y = actual height
986
+ ```
987
+
988
+ Source: Roblox Size Modifiers & Constraints docs (create.roblox.com/docs/ui/size-modifiers)
989
+
990
+ ---
991
+
992
+ ## 13. UI Styling (StyleEditor)
993
+
994
+ Roblox ships a CSS-like styling system (2025+). Use it for consistent theming.
995
+
996
+ ### Style Tokens
997
+
998
+ Named values (like CSS variables). Define once, reference everywhere.
999
+
1000
+ ### Style Rules with Selectors
1001
+
1002
+ ```luau
1003
+ -- Style rules can target by class, tag, name, state, modifier, query
1004
+ -- Similar to CSS selectors
1005
+ ```
1006
+
1007
+ ### When to Use StyleEditor vs Code Themes
1008
+
1009
+ | Approach | Best For |
1010
+ |----------|----------|
1011
+ | Studio StyleEditor | Static UI built visually, team collaboration on design |
1012
+ | Code-based UITheme module | Dynamic UI built in code, programmatic theming |
1013
+
1014
+ Both can coexist. StyleEditor is the "build in Studio" answer; code themes are the "build in code" answer.
1015
+
1016
+ Source: Roblox UI Styling docs (create.roblox.com/docs/ui/styling)
1017
+
1018
+ ---
1019
+
1020
+ ## 14. ScrollingFrame Patterns
1021
+
1022
+ ### AutomaticCanvasSize (Use This)
1023
+
1024
+ ```luau
1025
+ -- BAD: manually calculating canvas size
1026
+ scrollFrame.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y)
1027
+
1028
+ -- GOOD: engine handles it automatically
1029
+ scrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
1030
+ scrollFrame.ScrollingDirection = Enum.ScrollingDirection.Y
1031
+ ```
1032
+
1033
+ ### ScrollingFrame + UIListLayout
1034
+
1035
+ The most common pattern - a scrollable list:
1036
+
1037
+ ```luau
1038
+ local scrollFrame = Instance.new("ScrollingFrame")
1039
+ scrollFrame.Size = UDim2.new(1, 0, 1, 0)
1040
+ scrollFrame.BackgroundTransparency = 1
1041
+ scrollFrame.ScrollBarThickness = 6
1042
+ scrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
1043
+ scrollFrame.ScrollingDirection = Enum.ScrollingDirection.Y
1044
+ scrollFrame.Parent = container
1045
+
1046
+ local listLayout = Instance.new("UIListLayout")
1047
+ listLayout.FillDirection = Enum.FillDirection.Vertical
1048
+ listLayout.Padding = UDim.new(0, 8)
1049
+ listLayout.SortOrder = Enum.SortOrder.LayoutOrder
1050
+ listLayout.Parent = scrollFrame
1051
+
1052
+ -- Add items as children of scrollFrame
1053
+ -- They auto-arrange vertically, scrollFrame auto-sizes
1054
+ ```
1055
+
1056
+ ### ScrollingFrame + UIGridLayout
1057
+
1058
+ For scrollable grids (inventory, shop):
1059
+
1060
+ ```luau
1061
+ local scrollFrame = Instance.new("ScrollingFrame")
1062
+ scrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
1063
+ scrollFrame.ScrollingDirection = Enum.ScrollingDirection.Y
1064
+ scrollFrame.Parent = container
1065
+
1066
+ local gridLayout = Instance.new("UIGridLayout")
1067
+ gridLayout.CellSize = UDim2.new(0, 80, 0, 80)
1068
+ gridLayout.CellPadding = UDim2.new(0, 8, 0, 8)
1069
+ gridLayout.FillDirectionMaxCells = 4 -- 4 columns
1070
+ gridLayout.SortOrder = Enum.SortOrder.LayoutOrder
1071
+ gridLayout.Parent = scrollFrame
1072
+ ```
1073
+
1074
+ ### Elastic Overscroll
1075
+
1076
+ The bouncy effect on iOS/Android. Enabled by default. Disable if unwanted:
1077
+
1078
+ ```luau
1079
+ scrollFrame.ElasticBehavior = Enum.ElasticBehavior.Never
1080
+ ```
1081
+
1082
+ Source: Roblox Scrolling Frames docs (create.roblox.com/docs/ui/scrolling-frames)
1083
+
1084
+ ---
1085
+
1086
+ ## Sources
1087
+
1088
+ - Roblox Adaptive Design Guidelines: create.roblox.com/docs/production/publishing/adaptive-design
1089
+ - Roblox Size Modifiers & Constraints: create.roblox.com/docs/ui/size-modifiers
1090
+ - Roblox UI Styling: create.roblox.com/docs/ui/styling
1091
+ - Roblox Scrolling Frames: create.roblox.com/docs/ui/scrolling-frames
1092
+ - Roblox List & Flex Layouts: create.roblox.com/docs/ui/list-flex-layouts
1093
+ - Roblox Grid & Table Layouts: create.roblox.com/docs/ui/grid-table-layouts
1094
+ - DevForum: Designing UI - Tips and Best Practices (Roblox Staff)
1095
+ - DevForum: Design Mobile First
1096
+ - DevForum: GUI Optimization Tips
1097
+ - DevForum: epochzx - Container Frame Rule for ScreenGui
1098
+ - DevForum: uiuxartist (Roblox Staff) - Scale vs Offset guidelines
1099
+ - DevForum: PictureFolder - UI Design Tips and Best Practices (119 likes)
1100
+ - DevForum: Modern UI Colour Schemes - dark palette guidelines
1101
+ - Fusion: github.com/dphfox/Fusion (MIT)
1102
+ - Vide: github.com/centau/vide (MIT)
1103
+ - brockmartin/roblox-game-skill (MIT) - base content