Skip to content

GUIs and layout

Nova has the classic GUI stack: ScreenGui, SurfaceGui, BillboardGui, Frame, TextLabel, TextButton, ImageLabel, ImageButton, ScrollingFrame, TextBox, ViewportFrame (no), and the usual input events.

The important gap is the layout and modifier family. These classes are not implemented:

UIListLayout, UIGridLayout, UITableLayout, UIPadding, UIScale, UICorner, UIGradient, UIStroke, CanvasGroup, UIAspectRatioConstraint, UISizeConstraint, UITextSizeConstraint.

If your interface uses automatic layout, it will not lay itself out in Nova. The instances are skipped on load, so children keep whatever positions were saved in the file (often all zero).

What still works

  • AnchorPoint, Position, Size, SizeConstraint, AutomaticSize
  • ClipsDescendants, ZIndex, Visible, BackgroundColor3, BackgroundTransparency
  • TextLabel/TextButton/TextBox properties, fonts, text bounds, and input
  • ImageLabel/ImageButton, including 9-slice scaling
  • ScrollingFrame with CanvasSize
  • GuiObject:TweenPosition, TweenSize, TweenSizeAndPosition
  • GuiObject.InputBegan/InputChanged/InputEnded
  • ScreenGui.IgnoreGuiInset and the usual display properties

Replace UIListLayout with a layout pass

The smallest useful replacement is a function that positions siblings and updates the canvas size. Run it when the container changes or when a child is added/removed/resized.

-- ReplicatedStorage/Modules/Layout.lua
local Layout = {}

function Layout.column(container: ScrollingFrame, padding: number)
    local y = 0

    for _, child in ipairs(container:GetChildren()) do
        if child:IsA("GuiObject") and child.Visible then
            child.Position = UDim2.new(0, 0, 0, y)
            y += child.AbsoluteSize.Y + padding
        end
    end

    container.CanvasSize = UDim2.new(0, 0, 0, y)
end

function Layout.watch(container: ScrollingFrame, padding: number)
    local function relayout()
        Layout.column(container, padding)
    end

    container.ChildAdded:Connect(relayout)
    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()
end

return Layout
local Layout = require(game:GetService("ReplicatedStorage"):WaitForChild("Modules"):WaitForChild("Layout"))
local list = script.Parent:WaitForChild("List")

Layout.watch(list, 8)

For horizontal rows, swap the axes:

function Layout.row(container: ScrollingFrame, padding: number)
    local x = 0

    for _, child in ipairs(container:GetChildren()) do
        if child:IsA("GuiObject") and child.Visible then
            child.Position = UDim2.new(0, x, 0, 0)
            x += child.AbsoluteSize.X + padding
        end
    end

    container.CanvasSize = UDim2.new(0, x, 0, 0)
end

If your children use scale-based sizes, AbsoluteSize is only valid after the first render. Call the layout once on RunService.Heartbeat or after a task.defer, then connect the signals.

Replace UIAspectRatioConstraint

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

Call keepAspect(frame, 16 / 9) for a 16:9 panel. If the container also changes size, connect the container's AbsoluteSize as well.

Rounded corners, gradients, and strokes

Without UICorner, UIGradient, and UIStroke:

  • Rounded corners: use an ImageLabel with a 9-slice rounded-rect image, or keep square corners.
  • Gradients: bake the gradient into an image and use ImageLabel; set ImageColor3 for tint variations.
  • Strokes: put a slightly larger frame behind the content, or use a 9-slice border image.
  • Group tint/clip: set BackgroundTransparency/ImageTransparency on children individually; use ClipsDescendants on the container.

ViewportFrame

ViewportFrame is not implemented yet. For a 3D preview:

  • Put the model somewhere in the world and point a Camera at it.
  • Use a SurfaceGui on a part if the preview must be in the world.
  • For inventory-style previews, a pre-rendered thumbnail is usually simpler.

Debugging a layout that looks wrong

  1. Check the Output window for skipping unknown class 'UIListLayout' (or another modifier).
  2. Confirm the instances are actually missing in the Explorer.
  3. Print AbsoluteSize and Position after one frame; a zero size usually means the parent has not been sized yet.
  4. Re-run the layout after task.defer or on Heartbeat.

Next