Build a UI without UIListLayout¶
Nova does not implement UIListLayout. This tutorial builds a small, reusable
layout module and a scrolling list from scratch. It replaces the common
UIListLayout + UIPadding + UIAspectRatioConstraint combination.
What you need¶
ScreenGuiFrame(the list container)ScrollingFrame(for scrolling)TextButtonchildren
No layout or modifier classes are required.
1. Create the screen¶
-- StarterPlayer/StarterPlayerScripts/Hud.client.lua
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local screen = Instance.new("ScreenGui")
screen.Name = "Hud"
screen.ResetOnSpawn = false
screen.IgnoreGuiInset = true
screen.Parent = playerGui
local panel = Instance.new("Frame")
panel.Name = "Panel"
panel.AnchorPoint = Vector2.new(0.5, 0.5)
panel.Position = UDim2.fromScale(0.5, 0.5)
panel.Size = UDim2.new(0, 320, 0, 420)
panel.BackgroundColor3 = Color3.fromRGB(28, 30, 36)
panel.BorderSizePixel = 0
panel.Parent = screen
local list = Instance.new("ScrollingFrame")
list.Name = "List"
list.Position = UDim2.new(0, 12, 0, 52)
list.Size = UDim2.new(1, -24, 1, -64)
list.BackgroundTransparency = 1
list.BorderSizePixel = 0
list.ScrollBarThickness = 6
list.CanvasSize = UDim2.new(0, 0, 0, 0)
list.Parent = panel
local title = Instance.new("TextLabel")
title.Name = "Title"
title.Position = UDim2.new(0, 12, 0, 12)
title.Size = UDim2.new(1, -24, 0, 32)
title.BackgroundTransparency = 1
title.Font = Enum.Font.SourceSansBold
title.TextSize = 22
title.TextColor3 = Color3.fromRGB(240, 240, 245)
title.TextXAlignment = Enum.TextXAlignment.Left
title.Text = "Inventory"
title.Parent = panel
The panel uses a fixed size here. In a real UI, use scale/offset combinations
and recalculate on AbsoluteSize changes.
2. Add the layout module¶
Put this in ReplicatedStorage/Modules/Layout.lua:
local Layout = {}
local function isVisibleGui(child: Instance): boolean
return child:IsA("GuiObject") and child.Visible
end
function Layout.column(container: GuiObject, padding: number, startY: number?)
local y = startY or 0
for _, child in ipairs(container:GetChildren()) do
if isVisibleGui(child) then
child.Position = UDim2.new(0, 0, 0, y)
y += child.AbsoluteSize.Y + padding
end
end
if container:IsA("ScrollingFrame") then
container.CanvasSize = UDim2.new(0, 0, 0, y)
end
end
function Layout.row(container: GuiObject, padding: number, startX: number?)
local x = startX or 0
for _, child in ipairs(container:GetChildren()) do
if isVisibleGui(child) then
child.Position = UDim2.new(0, x, 0, 0)
x += child.AbsoluteSize.X + padding
end
end
if container:IsA("ScrollingFrame") then
container.CanvasSize = UDim2.new(0, x, 0, 0)
end
end
function Layout.watch(container: GuiObject, axis: "x" | "y", padding: number)
local function relayout()
if axis == "y" then
Layout.column(container, padding)
else
Layout.row(container, padding)
end
end
container.ChildAdded:Connect(function(child)
if child:IsA("GuiObject") then
child:GetPropertyChangedSignal("AbsoluteSize"):Connect(relayout)
child:GetPropertyChangedSignal("Visible"):Connect(relayout)
end
relayout()
end)
container.ChildRemoved:Connect(relayout)
container:GetPropertyChangedSignal("AbsoluteSize"):Connect(relayout)
for _, child in ipairs(container:GetChildren()) do
if child:IsA("GuiObject") then
child:GetPropertyChangedSignal("AbsoluteSize"):Connect(relayout)
child:GetPropertyChangedSignal("Visible"):Connect(relayout)
end
end
relayout()
return relayout
end
return Layout
AbsoluteSize is only meaningful after the first layout pass. watch calls
relayout once immediately and again whenever the container or a child changes
size.
3. Populate the list¶
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Layout = require(ReplicatedStorage:WaitForChild("Modules"):WaitForChild("Layout"))
local ITEMS = {
{ Name = "Wood", Amount = 42 },
{ Name = "Stone", Amount = 17 },
{ Name = "Iron", Amount = 9 },
{ Name = "Gold", Amount = 3 },
{ Name = "Diamond", Amount = 1 },
}
for _, item in ipairs(ITEMS) do
local row = Instance.new("TextButton")
row.Name = item.Name
row.Size = UDim2.new(1, 0, 0, 44)
row.BackgroundColor3 = Color3.fromRGB(40, 43, 52)
row.BorderSizePixel = 0
row.AutoButtonColor = true
row.Font = Enum.Font.SourceSans
row.TextSize = 18
row.TextColor3 = Color3.fromRGB(235, 235, 240)
row.TextXAlignment = Enum.TextXAlignment.Left
row.Text = string.format(" %s x%d", item.Name, item.Amount)
row.Parent = list
row.MouseButton1Click:Connect(function()
print("selected", item.Name)
end)
end
Layout.watch(list, "y", 8)
Layout.watch positions each row and sets CanvasSize to the total height.
4. Make it resize correctly¶
If the panel size is not fixed, relayout when the container changes:
For an aspect-ratio panel:
local function keepAspect(gui: GuiObject, aspect: number)
local function update()
local width = gui.AbsoluteSize.X
if width > 0 then
gui.Size = UDim2.new(0, width, 0, math.floor(width / aspect))
end
end
gui:GetPropertyChangedSignal("AbsoluteSize"):Connect(update)
update()
end
keepAspect(panel, 3 / 4)
5. Replacing the other modifiers¶
| Missing class | Replacement |
|---|---|
UIPadding |
Add the padding to Position/Size (the example uses UDim2.new(0, 12, 0, 52)). |
UIScale |
Multiply sizes in your layout function, or use scale-based UDim2. |
UICorner |
Use a 9-slice ImageLabel behind the content. |
UIStroke |
Add a slightly larger frame behind the content. |
UIGradient |
Bake the gradient into an image. |
UIAspectRatioConstraint |
Use keepAspect above. |
UISizeConstraint |
Clamp the computed size in your layout function. |
UITextSizeConstraint |
Clamp TextSize manually. |
6. Common pitfalls¶
- Everything is at (0,0). You did not call the layout function, or the
children were added after the call. Use
Layout.watch. - The list does not scroll.
CanvasSizewas not updated, or theScrollingFramehasScrollingEnabled = false. - Rows overlap.
AbsoluteSize.Yis zero because the row has not been sized yet. Set an explicit row height or defer the first pass withtask.defer. - The layout drifts. Re-running the layout is idempotent; make sure every child ends up at the computed position, not at its previous position.
- The UI is blurry or clipped. Check
ClipsDescendantsand the parent'sAbsoluteSize.