Flux
Types
Node type
type Node<T> = Graph.Node<T>Reactive type
type Reactive<T> = Graph.Reactive<T>Properties
Bind Module
Flux.Bind: BindBind: low-level hydration primitives behind Flux.model, the _EVENT directive, and child parenting. Binds a node to or from a Roblox Instance property or attribute.
Color Module
Flux.Color: ColorColor: a perceptual Color3 toolkit working in gamut-clipped Oklab. Provides lighten/darken, mix, rotateHue, and WCAG contrast/readable.
Defaults Module
Flux.Defaults: DefaultsDefaults: per-class default properties auto-applied to every Roblox Instance built by Flux.new. Mutable.
Find Module
Flux.Find: FindFind: Instance selectors for hydration. Child, Descendant, Ancestor, Query, and class-filtered variants apply props to matched descendants of a Roblox Instance.
Flags Module
Flux.Flags: FlagsFlags: global toggles. Flags.defaults enables or disables the automatic per-class defaults system applied by Flux.new.
Graph Module
Flux.Graph: GraphGraph: the low-level reactive graph. Most members are surfaced flat on Flux (Flux.read, Flux.retrack, Flux.untrack, …); reach into the namespace for Flux.flush.
Interact Module
Flux.Interact: InteractInteract: transient interaction state. exclusive groups keep at most one boolean node active per group (one open menu, one hovered control); pointIn and sunk hit-test pointer coordinates against Roblox GuiObject bounds and occlusion.
Layout Module
Flux.Layout: LayoutLayout: layout and appearance helpers (padding, corner, stroke, list, grid, aspectRatio, flex) that return ready-to-parent Roblox Instances with reactive arguments.
Motion Module
Flux.Motion: MotionMotion: animation. spring and tween return animated nodes; it also hosts the Color toolkit and step.
Responsive Module
Flux.Responsive: ResponsiveResponsive: viewport-driven UI. viewport, scale, breakpoint, and safeArea are shared reactive nodes that update as the Roblox Camera viewport changes.
Store Module
Flux.Store: StoreStore: deep reactive proxies over plain tables. Write a nested field and only the nodes whose leaves changed re-run.
viewport Read Only from Responsive
Flux.viewport: Graph.Node<Vector2>The active camera's ViewportSize as a node, live on the client.
scale Read Only from Responsive
Flux.scale: Graph.Node<number>UIScale factor derived from viewport; 1.0 at config.reference.
safeArea Read Only from Responsive
Flux.safeArea: Graph.Node<SafeAreaInsets>Topbar / Core-UI inset pixels from GuiService:GetGuiInset().
clean Module
Flux.clean: Cleandefault Read Only from Conditional
Flux.default: anySentinel key for Flux.switch maps: the factory stored under this key mounts whenever the source value matches no other key, the conditional's catch-all branch. Unmatched values share this stable key, so switching between two different unmatched values does not rebuild the branch.
Functions
edit
Flux.edit<T>(instance: T & Instance)Hydrates an existing Roblox Instance, applying reactive properties, event handlers, and directives just like Flux.new, but onto an Instance you already have. Returns a function that takes the property table. Called inside a Flux.scope, the bindings are owned by that scope and disconnected when it is destroyed.
new
Flux.new<T>(className: T | keyof<Type.Creatable>)Creates a new Roblox Instance of className with per-class defaults applied, then returns a function that takes a property table of static values, reactive nodes, children, and directives. Created inside a Flux.scope, the instance is owned by that scope and destroyed with it.
flush from Graph
Flux.flush(index: number?)Drains the pending-effect queue immediately, instead of waiting for the deferred flush. Mainly useful in tests to make effects fire synchronously after a write.
strict from Graph
Flux.strict(on: boolean?): booleanSetter/getter for dev-mode strict checks. Pass true/false to set the flag, call with no argument to read it; defaults on in development, off under production. When on:
- every reactive scope (computeds and effects alike) runs a full second time, surfacing non-idempotent bodies in dev.
- reactive cycles (re-entering a running scope) and destroying the running scope error.
- instance binding writes are checked individually: a failure reports the instance, key, and value, and no longer skips the node's remaining bindings.
scope from Graph
Runs fn in a fresh owner scope, returning (scope, result): the scope node and whatever fn returned. Computeds, effects, instances, and cleanup callbacks created inside are owned by that scope and torn down together when you call scope:Destroy(); plain signals are not owned and are simply left for the GC.
signal from Graph
Flux.signal<T>(initial: T, equals: ((T, T) -> boolean)?): Node<T>Creates a writable signal holding initial.
- Reading it inside a reaction subscribes; writing a new value re-runs dependents.
- An optional
equalsshort-circuits propagation when the new value compares equal to the old. - A signal is NOT owned by the enclosing scope (unlike a computed/effect); it is reclaimed by the GC when you drop it, not torn down on scope destroy.
computed from Graph
Flux.computed<T>(fn: () -> T, equals: ((T, T) -> boolean)?): Node<T>Creates a lazily-cached derived node from fn.
- it re-evaluates only when read after a tracked dependency changed
- an optional
equalsshort-circuits propagation on an equal result
effect from Graph
Flux.effect<T>(fn: (T) -> ()): Node<T>Creates an effect node derived from fn.
- deferred: queues
fnto run on the next flush, then re-runs it whenever a tracked dependency changes - returns the effect node; call
node:Destroy()to stop it
getOwner from Graph
Flux.getOwner(): node?Returns the current owner scope (a node), or nil at the root.
- Capture it to re-establish ownership in deferred or async work via
Flux.withOwner.
withOwner from Graph
Flux.withOwner<T>(node: Node<any>, fn: () -> ...T): TRuns fn with node installed as the current owner scope, so the computeds, effects, and cleanup callbacks created inside attach to it and tear down on its lifetime; dependency tracking is left untouched (tracking-transparent).
cleanup from Graph
Flux.cleanup(obj: any)Registers fn to run right before the enclosing reactive scope re-evaluates (and once more when it is destroyed), letting an effect tear down whatever it set up on the previous run: disconnect events, cancel work, release instances.
- Must be called from inside a computed or effect body;
fnreceives the node's prior value.
isNode from Graph
Flux.isNode(object: unknown): booleanReturns true when obj is a reactive node. Use it to branch on node-or-value props inside a component before deciding whether to read reactively.
isReactive from Graph
Flux.isReactive(): booleanReturns true when called from within a tracking computed or effect body, where reads subscribe the running computation.
raw from Graph
Flux.raw(object: any): anyReads a node's current value without tracking it; a non-node object passes straight through, so it is safe to call on a prop that may or may not be reactive.
read from Graph
Flux.read(object: any): anyReads a node's current value with tracking, just like node:get(); a non-node object passes straight through, making it the idiomatic way to read a prop that may be a plain value or a node.
untrack from Graph
Flux.untrack<T>(fn: (() -> ...T)?): ...TSuspends dependency tracking for the duration of fn (reads inside don't subscribe the enclosing computed/effect).
- Called bare as
untrack()it suspends imperatively until a matchingtrack()or the nextupdate().
retrack from Graph
Flux.retrack()Re-enables dependency tracking after a bare Flux.untrack() call suspended it.
- Use the imperative
untrack()/track()pair to fence a run of un-tracked reads inside a computed or effect.
on from Bind
Flux.on<D, R>(deps: any, fn: (input: any, prevInput: any?, prevResult: R?) -> R, defer: boolean?): (prevResult: R?) -> R?Builds an explicit-dependency reaction: it tracks only deps (a single node/function/value or an array of them) and runs fn untracked whenever one changes, so reads inside fn never become dependencies. Wrap it in a computed or effect; fn receives the current input, the previous input, and its own previous result. Set defer to skip the first run and react only to subsequent changes.
Parameters
deps: The dependency, or array of dependencies, to react to.fn: The reaction, run untracked, receiving(input, prevInput, prevResult).defer: Skip the initial run, firing only on later changes.
listen from Bind
Flux.listen<T>(node: Graph.Node<T>, fn: (value: T) -> ()): () -> ()Registers fn to run on each update of the node, with RBXScriptSignal:Connect semantics: each invocation runs on its own thread (via task.spawn), so callbacks may yield freely. Returns an unsubscribe. For synchronous reactive observation, use an effect instead.
model from Bind
Flux.model<T>(node: Graph.Node<T>): TWraps a reactive node for two-way binding. Assigning Flux.model(node) to a property (or attribute) drives the Instance from the node AND writes the node back whenever the user changes that property, the terse equivalent of assigning the node and repeating it under _EVENT. Only sound for properties that fire a changed signal (e.g. TextBox.Text).
attr from Directive
Flux.attr(attribute: { [string]: any } | string, value: any?): anyOne-off form of the _ATTR directive: place Flux.attr("Name", value) or Flux.attr { Name = value } anywhere in the array portion of a property table to set or bind Roblox attributes on the Instance. Accepts the same values as _ATTR (static values, reactive nodes, functions, and Flux.model).
event from Directive
Flux.event(event: { [string]: any } | string, handler: ((...any) -> ()) | Node | nil): anyOne-off form of the _EVENT directive: place Flux.event("Name", handler) or Flux.event { Name = handler } anywhere in the array portion of a property table to connect listeners or bind values from the Instance into nodes. Accepts the same values as _EVENT, including a nested _ATTR map for attribute-changed listeners.
onDestroy from Directive
Flux.onDestroy(...: any): anyOne-off form of the _CLEAN directive: place Flux.onDestroy(...) anywhere in the array portion of a property table to tie extra teardown (functions, connections, instances, scopes, or arrays of these) to the Instance's Destroying lifetime. Distinct from Flux.cleanup, which registers a callback on the current reactive owner instead.
ref from Directive
One-off form of the _REF directive: place Flux.ref(nodeOrCallback) anywhere in the array portion of a property table to receive the built Instance. A node is set to the instance; a callback receives it and may return a cleanup value tied to the instance's lifetime.
tag from Directive
Flux.tag(...: tagValue): anyOne-off form of the _TAG directive: place Flux.tag(...) anywhere in the array portion of a property table to manage CollectionService tags on the Instance. Each argument accepts the same values as _TAG: a string, a reactive node or function whose value is a tag or array of tags (diffed on change), or an array of any of these.
context from Context
Flux.context<T>(default: T): Context<T>Creates a dynamically-scoped value with the given default, read by any code running under a provide without threading it through arguments. Outside any active provide, reads return default. Pair with components to share theme, services, or the current user down a tree.
spring from Motion
Flux.spring<T>(target: T, frequency: number?, damping: number?): Graph.Node<T> & Spring<T>Creates a node that springs toward target, animating any supported value type (numbers, Vector2, UDim2, Color3, …). When target is reactive the spring re-targets on every change, carrying its momentum through. Read the returned node anywhere to subscribe to the live animated value.
Parameters
target: The goal value, static or a reactive node; the spring eases toward it.frequency: Oscillations per second, where higher snaps faster. Defaults to a tuned value; may be reactive.damping: The damping ratio:1is critically damped (no overshoot),<1springs past and settles,>1is sluggish. May be reactive.
tween from Motion
Flux.tween<T>(target: T, tweenInfo: TweenInfo?): Graph.Node<T>Creates a node that animates toward target over a fixed duration described by a TweenInfo (easing style, direction, time, repeats). When target or the TweenInfo is reactive the tween restarts toward the new goal. Read the returned node to subscribe to the live animated value. Prefer a Flux.spring for momentum-driven, interruptible motion.
padding from Layout
Flux.padding(value: Length | PaddingSides | Graph.Node<any> | (() -> Length | Insets)): UIPaddingCreates a UIPadding from a single length (applied to all sides), a per-side { top, bottom, left, right, x, y } table, or a reactive node or function yielding a length or a { top, bottom, left, right } struct such as Flux.safeArea. Plain number values are offset pixels; a UDim is used as-is. Reactive sides bind and update in place; a reactive source may change shape between updates, and nil reads as no padding.
corner from Layout
Creates a UICorner rounding the parent's corners. radius is offset pixels or a UDim, optionally reactive; omit it for the engine default (8 px).
stroke from Layout
Flux.stroke(config: StrokeConfig?): UIStrokeCreates a UIStroke outlining the parent. thickness (px), color, and transparency bind reactively; mode is "contextual" (text outline on text objects, border otherwise) or "border", and joins is "round"/"bevel"/"miter", each also accepting the raw Enum or a reactive source.
aspectRatio from Layout
Flux.aspectRatio(ratio: Reactive<number>, aspectType: Reactive<"fit" | "scale" | Enum.AspectType>?, dominantAxis: Reactive<"width" | "height" | Enum.DominantAxis>?): UIAspectRatioConstraintCreates a UIAspectRatioConstraint constraining a frame's width-to-height ratio. Every argument may be reactive.
Parameters
ratio: The aspect ratio; may be a static number or a reactive node.aspectType:"fit"(FitWithinMaxSize) or"scale"(ScaleWithParentSize), or a rawEnum.AspectType.dominantAxis:"width"or"height", or a rawEnum.DominantAxis; an unknown string warns and falls back.
list from Layout
Flux.list(config: ListConfig?): UIListLayoutCreates a UIListLayout that arranges siblings in a line with flexbox semantics. gap is the spacing between items (offset px or a UDim); direction is "x"/"y". align sets the cross-axis and justify the main-axis value: "start"/"center"/"end" align items, "between"/"around"/"evenly" distribute the free space, and "stretch"/"fill" resize items to fill the axis. horizontalAlign/verticalAlign are explicit axis-pinned overrides, wraps flows items onto multiple lines, and lineAlign aligns items within their wrapped line. Every value, including direction, may be reactive; alignments re-route when a reactive direction flips.
grid from Layout
Flux.grid(config: GridConfig?): UIGridLayoutCreates a UIGridLayout that arranges siblings in a uniform grid. cell is the cell size and gap the spacing, each a Vector2, a number (offset px), or a UDim2. fill is the flow direction "x"/"y"; align/justify map to horizontal/vertical alignment; maxCells caps cells per line. Every value may be reactive.
flex from Layout
Flux.flex(mode: Reactive<FlexMode | Enum.UIFlexMode>?): UIFlexItemCreates a UIFlexItem controlling how a child grows or shrinks inside a flex UIListLayout. mode is "fill" (the default when omitted), "grow", "shrink", or "none", or a raw Enum.UIFlexMode, optionally reactive.
exclusive from Interact
Flux.exclusive(): GroupCreates an exclusive group: at most one boolean node is active per group, so activating a member deactivates the previous one. One group per concern (open menus, hovered controls, modal dialogs) replaces the "close everything else first" bookkeeping those UIs otherwise need.
pointIn from Interact
Flux.pointIn(x: number, y: number, object: GuiObject): booleanWhether the point (x, y) lies within object's absolute bounds (inclusive, ignoring rotation). Coordinates are inset-relative, matching InputObject.Position and AbsolutePosition.
sunk from Interact
Flux.sunk(x: number, y: number, object: GuiObject): booleanWhether input at (x, y) would be claimed by an Active gui (or GuiButton) rendered above object, e.g. dropping a hover state while a floating menu covers the control. Outside a BasePlayerGui (a plugin dock, a SurfaceGui in workspace) nothing can claim the point, so this yields false.
async from Async
Flux.async<S, T>(source: any, fetcher: ((S?, T, any) -> T) | T?, initialValue: T?): Async<T>Creates a non-blocking asynchronous node that runs yielding work off the reactive graph and exposes its progress as four nodes: .data, .error, .loading, and .state. A race guard discards stale results, so only the latest fetch ever writes back.
Parameters
source: A tracked input: a node or a computation whose value is passed to the fetcher; the fetcher re-runs whenever it changes. Omit it (pass the fetcher first) for a one-shot fetch. Anil/falsesource value gates the fetch off until it becomes truthy.fetcher: The untracked yielding work, called asfetcher(value, previous, refetching)wherepreviousis the previously resolved data andrefetchingflags a manual refetch. Reactive reads inside it are not tracked as dependencies.initialValue: The starting.datavalue before the first fetch resolves.
safe from Safe
Flux.safe<T>(tryFn: () -> T, fallback: T | (err: unknown) -> T, equals: ((a: T, b: T) -> boolean)?): Graph.Node<T>Creates a computed that evaluates tryFn and, if it throws, falls back to fallback, re-running and recovering automatically whenever its tracked dependencies change. fallback may be a static value or a recovery function receiving the caught error; a recovery function runs untracked so it cannot accidentally subscribe to extra dependencies. Pass equals to customize change detection (default ==).
catch from Safe
Flux.catch<T>(fn: () -> T, handler: (err: unknown) -> T): TSynchronously runs fn and, if it throws, recovers with handler: a plain try/recover with no reactive node created. Both fn and handler run untracked, so reads inside them don't subscribe the surrounding computed or effect. If handler itself throws, that error propagates.
forValue from For
Flux.forValue<T, U>(list: Node<{ T }> | { T }, mapFn: (index: Node<number>, value: T) -> U): Node<{ U }>Maps an array by value (keyed): each item's mapped result is cached against the value itself, so results stay stable across reorders and only the item's reactive index node updates when it moves. Values must be unique: the value is the cache key, so duplicates collapse onto one mapped instance; for primitives, duplicates, or position-based churn use Flux.forIndex instead.
Parameters
list: The reactive array (a node or plain array) to map over.mapFn: Called once per unique value; receives a reactive index node and the value.
forIndex from For
Flux.forIndex<T, U>(list: Node<{ T }> | { T }, mapFn: (index: any, item: Node<T>) -> U): Node<{ U }>Maps an array by index (unkeyed): the result at each position is built once and reused, with the item passed as a reactive node that updates in place as the value at that index changes. Reach for this over Flux.forValue when the array holds primitives, has duplicates, or churns by position rather than identity.
Parameters
list: The reactive array (a node or plain array) to map over.mapFn: Called once per index; receives the index and a reactive item node.
selector from Selector
Flux.selector<S, K>(source: Graph.Node<S> | (() -> S) | S, equals: ((sourceValue: S, key: K) -> boolean)?): Selector<K>Creates an O(1) keyed selector for efficiently tracking which of many keys matches a single selected value: selecting a row, a tab, or a focused item. The returned object is callable: selector(key) reactively reads whether key is selected, so flipping the selection re-runs only the two affected keys instead of every observer.
Parameters
source: The tracked selection: a node, a() -> Scomputed, or a plain value.equals: Tests the source value against a key (default==). The default path flips at most two per-key nodes per change (O(1)); a custom comparator re-tests every live key per change (O(live keys)).
show from Conditional
Flux.show<T, F>(condition: Node<any> | (() -> any) | any, component: (value: () -> any) -> T, fallback: ((value: () -> any) -> F)?): Node<any>Mounts component while condition is truthy, swapping to fallback (if given) when it turns falsy. The chosen factory runs once per mount in its own branch scope (the effects and cleanups it registers are torn down automatically on the next swap) and receives a value accessor for the live condition value. Returns a node usable at a numeric index of Flux.new / Flux.edit.
Parameters
condition: The reactive source; re-runs only when its truthiness flips, not on every change.component: Branch factory mounted whileconditionis truthy; receives(value).fallback: Optional branch factory mounted whileconditionis falsy; receives(value).
showKeyed from Conditional
Flux.showKeyed<T, F>(condition: Node<any> | (() -> any) | any, component: (value: () -> any) -> T, fallback: ((value: () -> any) -> F)?): Node<any>Identity-keyed variant of Flux.show: instead of rebuilding only when condition's truthiness flips, it rebuilds whenever the value's identity changes, so swapping one truthy value for another remounts the branch with fresh state. The chosen factory runs in its own branch scope and receives a value accessor; since the value is fixed for the branch's lifetime, reading value() in the factory body yields it directly. Mirrors SolidJS's <Show keyed>.
Parameters
condition: The reactive source; re-runs whenever its value changes identity, not just truthiness.component: Branch factory mounted whileconditionis truthy; receives(value).fallback: Optional branch factory mounted whileconditionis falsy; receives(value).
switch from Conditional
Flux.switch<K>(source: Node<K> | (() -> K) | K)Mounts the branch keyed by source's current value. Curries: pass the reactive source, then a map of value → factory; the matching factory runs once in its own branch scope and receives a value accessor. For a keyed branch value() is the matched key (fixed for the branch's lifetime); under Flux.default (which mounts for any unmatched value), value() tracks the live unmatched value. Returns a node usable at a numeric index of Flux.new / Flux.edit.
store from Store
Flux.store<T>(initialState: T): TWraps a plain table in a deep reactive proxy: reading a field inside a computed or effect subscribes to that field, and assigning a new value re-runs only the dependents of the leaves that changed. Nested plain tables are proxied recursively on access; metatable'd values (Flux nodes, class instances) stay atomic. Available as Flux.store.
props from Wrap
Flux.props<T, O>(defaults: T, overrides: O?): Wrapped<T>Builds a component props table from a defaults schema and the caller's overrides, keeping only keys declared in defaults (unknown override keys are left for instance hydration) and passing the merged result through wrap. Neither input is mutated: nested plain tables are deep-copied per call, so instances never share state through the schema or a reused override table.
wrap from Wrap
Flux.wrap<T>(obj: T): Wrapped<T>Makes a table reactive in place: every plain leaf becomes a node, recursing into nested plain tables and leaving existing nodes and metatabled tables (a class instance, a proxy) untouched. The table is returned with its type preserved, so wrap({ hp = 100 }).hp() is a number. A non-table value returns a single node holding it.
Metamethods
__call
Flux.__call<T>(self: any, obj: (() -> T) | T, effectOrProperty: boolean? | string?): Graph.Node<T>The terse constructor: Flux(value) wraps a static value, Flux(fn) a computed, Flux(fn, true) an effect, and Flux(instance, property) binds a new node from a Roblox Instance property or attribute.
Parameters