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:
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:
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:
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), 600–1024 (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:
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:
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:
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 }| Field | Default | Meaning |
|---|---|---|
reference | Vector2(1920,1080) | The resolution at which scale is 1.0. |
mode | "min" | "min" (fit) · "width" · "height" · "max" (cover) · "diagonal". |
min / max | nil | Optional 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:
-- 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:
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.