Skip to content

Home › Utilities

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, 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, alongside the rest of this page's API.)

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.Responsive.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.Responsive.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).

Platform

Where breakpoints answer "how big", Flux.Responsive.platform answers "what device": "vr", "console" (ten-foot interface), "pc" (keyboard and mouse), or "mobile". It is a plain string, not a node: the device class is detected once at startup, and off-client (server, Studio edit mode, headless) it is always "pc":

luau
new "TextButton" {
    -- Console and touch devices deserve bigger hit targets.
    Size = if Flux.Responsive.platform == "pc" then UDim2.fromOffset(160, 36) else UDim2.fromOffset(200, 48),
}

TIP

Switch layout on breakpoint, which tracks the window live, and switch input affordances (hit-target size, button prompts, hover behavior) on platform, which reflects what the device is rather than how big its window happens to be.

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 (on the client; elsewhere it holds 1920×1080 and stays writable, so exotic hosts can drive it themselves). 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).
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 },
}

Container queries & plugin docks

The factories also take a viewport override: any reactive source yielding a size. The derived node then tracks that surface instead of the screen: CSS container queries, in effect. Bind an instance's AbsoluteSize with the terse constructor and a Studio plugin dock (or a SurfaceGui, or any frame) becomes its own responsive context:

luau
local dockSize = Flux(dockWidget, "AbsoluteSize")

local dockScale = Flux.Responsive.scaleOf {
    viewport = dockSize,
    reference = Vector2.new(400, 600),
}
local dockBreakpoint = Flux.Responsive.breakpointOf {
    viewport = dockSize,
    breakpoints = { phone = 320, tablet = 480 },
}

This is the intended path for plugin UIs: the shared Flux.viewport/Flux.safeArea singletons wire to the camera and GuiService on the client only (a server or Studio edit session keeps their defaults), while dock-driven nodes never depend on them.

NOTE

Only the engine wiring (camera, GuiService) is Roblox-specific. The derivation math runs anywhere: hand the factories a plain { X, Y } source and they work in headless Luau, which is how Flux unit-tests them.