Skip to content

Layout

Roblox arranges and constrains UI through child instances: UIListLayout, UIPadding, UIAspectRatioConstraint, and friends. Writing them out longhand is verbose, especially the ones whose properties are UDims. Flux's layout helpers are thin, typed wrappers that return those instances, so they drop straight into a children list alongside everything else:

luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Flux = require(ReplicatedStorage.Flux)
local new = Flux.new

new "Frame" {
    Size = UDim2.fromOffset(300, 0),
    AutomaticSize = Enum.AutomaticSize.Y,

    Flux.padding(16),
    Flux.list { gap = 8 },

    new "TextLabel" { Text = "First" },
    new "TextLabel" { Text = "Second" },
}

Because each helper just returns an instance, it composes with the normal builder: forValue, selectors, and _CLEAN all keep working, and the parent Frame keeps full strict-typed property autocomplete. The helpers are also available under the Flux.Layout namespace.

Conventions

Three rules apply across every helper:

  • Bare numbers are offset pixels. Flux.padding(8) and gap = 8 become UDim.new(0, 8). Pass a UDim explicitly if you want scale.
  • Friendly strings instead of Enums. direction = "x", align = "center", Flux.flex("fill"): terser than the full Enum, and still strictly typed. You can pass a raw Enum anywhere a string is accepted.
  • Any value can be reactive. A Node or a function works wherever a static value does, and binds reactively, so Flux.padding(Flux.safeArea) and Flux.list { gap = animatedGap } just work, and clean up when the instance is destroyed.

padding

Returns a UIPadding. Accepts one length for all sides, a per-side table, or a reactive struct node:

luau
Flux.padding(8)                      -- all four sides
Flux.padding { x = 16, y = 8 }       -- horizontal / vertical
Flux.padding { top = 8, left = 16 }  -- only the sides you name
Flux.padding(Flux.safeArea)          -- bind a {top,bottom,left,right} node

list

Returns a UIListLayout (inheriting Flux's SortOrder = LayoutOrder default).

luau
Flux.list {
    gap = 8,             -- spacing between items (offset px)
    direction = "x",     -- "x" (horizontal) | "y" (vertical, default)
    align = "center",    -- cross-axis alignment
    justify = "start",   -- main-axis alignment (along `direction`)
    wraps = true,
}

align and justify follow flexbox semantics: align is the cross axis and justify the main axis (the one items flow along). Because their target axis depends on direction, they take the axis-agnostic strings "start" | "center" | "end". To set an axis directly, or to pass a raw Enum, use horizontalAlign / verticalAlign instead.

grid

Returns a UIGridLayout. cell and gap accept a Vector2 or number (offset px) or a UDim2:

luau
Flux.grid {
    cell = Vector2.new(100, 100),  -- CellSize
    gap = 8,                       -- CellPadding
    fill = "x",                    -- "x" | "y"
    align = "center",              -- HorizontalAlignment
    justify = "center",            -- VerticalAlignment
    maxCells = 4,                  -- FillDirectionMaxCells
}

Unlike list, grid's align and justify always map to the horizontal and vertical axes respectively (not the direction-relative cross/main pair) since a grid flows along both axes at once.

aspectRatio

Returns a UIAspectRatioConstraint:

luau
Flux.aspectRatio(16 / 9)
Flux.aspectRatio(1, "scale", "height")  -- ratio, aspectType, dominantAxis

aspectType accepts "fit" / "scale"; dominantAxis accepts "width" / "height".

sizeConstraint

Returns a UISizeConstraint. Either bound may be omitted:

luau
Flux.sizeConstraint(Vector2.new(200, 100), Vector2.new(600, 400))
Flux.sizeConstraint(nil, Vector2.new(600, 400))  -- max only

Pure UIScale-based scaling shrinks everything uniformly; pairing it with a sizeConstraint keeps small screens from collapsing UI below a usable size. See Responsive.

flex

Returns a UIFlexItem for items inside a flex UIListLayout:

luau
new "Frame" {
    Flux.list { direction = "x" },

    new "TextLabel" { Text = "Fixed" },
    new "Frame" { Flux.flex("fill") },  -- "fill" | "grow" | "shrink" | "none"
}

NOTE

These helpers create Roblox instances, so they run under Roblox (not headless Luau). A bad friendly string warns and falls back to a sensible default rather than erroring, but in --!strict mode the typed string-literal unions catch the typo before it ever runs.