Skip to content

Responsive

Roblox UI authored in fixed offset pixels looks right on the screen you built it on and wrong everywhere else. Flux's responsive layer exposes the screen as a set of reactive values you read (Flux.viewport, Flux.scale, Flux.breakpoint, and Flux.safeArea), so your layout reacts to the device the same way every other binding reacts to state.

Everything here is derived from one shared Flux.viewport node, kept in sync with the active camera. There are no factories to call and nothing to mount: read the value, bind it, done. (They are also available under the Flux.Responsive namespace.)

Scale

Flux.scale is a Node<number> where 1.0 means the viewport matches the reference resolution. Bind it to a UIScale and your whole UI grows and shrinks proportionally:

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

new "ScreenGui" {
    Parent = playerGui,

    new "Frame" {
        Size = UDim2.fromOffset(800, 600),

        -- One UIScale scales the entire subtree to fit the screen.
        new "UIScale" { Scale = Flux.scale },
    },
}

Because reactive nodes support operator overloading, you can also fold the factor directly into a calculation:

luau
new "TextLabel" {
    -- Scale a fixed design size by the current factor.
    Size = function()
        return UDim2.fromOffset(200 * Flux.scale, 50 * Flux.scale)
    end,
}

By default scale is computed as math.min(viewport.X / reference.X, viewport.Y / reference.Y): the fit mode, which guarantees nothing ever overflows the screen. The reference resolution defaults to 1920×1080 and there are no clamps. See Configuration to change any of this.

Breakpoint

Flux.breakpoint is a Node<"phone" | "tablet" | "desktop"> derived from viewport width. Use it to switch layout, not just shrink it. Pair it with Flux.switch:

luau
Flux.switch(Flux.breakpoint) {
    phone = function()
        return CompactLayout()
    end,
    tablet = function()
        return CompactLayout()
    end,
    desktop = function()
        return WideLayout()
    end,
}

NOTE

Breakpoints are size classes, not hardware detection: they come from viewport width, so a small windowed desktop client falls into "phone". That is usually exactly what you want: a phone-sized window deserves the phone layout. The default thresholds are < 600 (phone), 6001024 (tablet), and 1024 (desktop).

Safe area

Flux.safeArea is a Node<{ top: number, bottom: number, left: number, right: number }> of inset pixels from the Roblox topbar and reserved Core-UI regions (GuiService:GetGuiInset()). The struct maps one-to-one onto a UIPadding, and Flux.padding accepts the node directly:

luau
new "Frame" {
    Size = UDim2.fromScale(1, 1),

    -- Keep content clear of the topbar / notch, reactively.
    Flux.padding(Flux.safeArea),

    -- ...content...
}

TIP

Device notches and rounded corners are handled declaratively by ScreenGui.ScreenInsets. Flux.safeArea covers the topbar/Core-UI inset.

Viewport

Flux.viewport is the Node<Vector2> everything else derives from: the current ViewportSize of workspace.CurrentCamera, kept in sync as the window resizes or the camera changes. Read it when you need the raw size:

luau
local isPortrait = Flux(function()
    return Flux.viewport().Y > Flux.viewport().X
end)

Configuration

Tuning lives in the global Flux.Responsive.config table. Set it once at startup, before your UI is built:

luau
Flux.Responsive.config.reference = Vector2.new(1280, 720)
Flux.Responsive.config.mode = "height"
Flux.Responsive.config.min = 0.5  -- never shrink below half size
Flux.Responsive.config.max = 2    -- never grow past double
Flux.Responsive.config.breakpoints = { phone = 700, tablet = 1100 }
FieldDefaultMeaning
referenceVector2(1920,1080)The resolution at which scale is 1.0.
mode"min""min" (fit) · "width" · "height" · "max" (cover) · "diagonal".
min / maxnilOptional clamps on the scale factor.
breakpoints{phone=600, tablet=1024}Upper-bound widths for phone and tablet; anything wider is desktop.

Per-surface overrides

For a second reference resolution or a SurfaceGui with a different effective size, use the factory escape hatches. They take a partial config (merged over your global Flux.Responsive.config, so unspecified fields inherit whatever you set there), and return a fresh node:

luau
-- A scale node tuned for a wide HUD, width-driven and clamped.
local hudScale = Flux.Responsive.scaleOf {
    mode = "width",
    min = 0.75,
}

local tabletFirst = Flux.Responsive.breakpointOf {
    breakpoints = { phone = 480, tablet = 900 },
}

Pure helpers

The derivation math is also exposed as pure functions that take plain numbers, handy for one-off calculations and unit tests:

luau
Flux.Responsive.scaleFor(1280, 720)        -- → number
Flux.Responsive.breakpointFor(800)         -- → "tablet"
Flux.Responsive.scaleFor(1280, 720, { mode = "width" })

NOTE

The reactive nodes read the engine (camera, GuiService), so they only update under Roblox. The pure scaleFor / breakpointFor helpers and all derivation logic run anywhere, including headless Luau, which is how Flux tests them.