Skip to content

Home › Utilities

Interact ​

Flux.Interact provides read-only boolean nodes for common UI interaction states (hover, press, selected, focused) along with pointer hit-test predicates (pointIn, sunk, covered).

luau
local button = Flux.new "TextButton" {
    Size = UDim2.fromOffset(180, 44),
    Text = "Play",
}

local hovered = Flux.Interact.hover(button)
local pressed = Flux.Interact.press(button)

Flux.edit(button) {
    BackgroundColor3 = function()
        if pressed() then
            return Color3.fromRGB(40, 90, 200)
        elseif hovered() then
            return Color3.fromRGB(70, 130, 255)
        end
        return Color3.fromRGB(55, 110, 230)
    end,
}

Every state node starts listening immediately and cleans up automatically when the node, its owning Scope, or object is destroyed, settling to false.

hover ​

Flux.Interact.hover(object, options?) returns a Node<boolean> that is true while a mouse pointer or touch contact is inside object with no input-claiming GUI drawn above it (checked via sunk).

Because hover re-checks occlusion every frame while the pointer is inside object:

  • A floating menu or modal opening on top of object immediately drops the hover state.
  • Scrolling object out from under a stationary pointer drops the hover state.
  • Closing an obstructing overlay over a stationary pointer restores the hover state.
luau
local hovered = Flux.Interact.hover(button)

-- Container hover: stay hovered even when the pointer is over a child button inside `row`
local rowHovered = Flux.Interact.hover(row, { within = true })

Pass { within = true } when tracking a container (such as a card, list row, or panel) so child buttons inside object do not count as covering it (see covered).

press ​

Flux.Interact.press(object) returns a Node<boolean> that is true while object is actively held down:

  • Pointer / Touch: Starts on MouseButton1 or Touch inside object (unless covered by an input-sinking GUI above it) and remains true until that specific input ends anywhere on screen, so dragging slightly off a button never leaves it stuck.
  • Gamepad / Keyboard: Starts when ButtonA, Return, or KeypadEnter is pressed while object is GuiService.SelectedObject (and no TextBox has focus).
  • Opening the Roblox escape menu (GuiService.MenuIsOpen) releases any active press immediately.
luau
local pressed = Flux.Interact.press(button)

selected ​

Flux.Interact.selected(object) returns a Node<boolean> tracking whether object is currently highlighted by gamepad or keyboard UI navigation (GuiService.SelectedObject):

luau
local isSelected = Flux.Interact.selected(button)

focused ​

Flux.Interact.focused(textBox) returns a Node<boolean> tracking whether textBox currently has keyboard focus (TextBox:IsFocused(), Focused, and FocusLost):

luau
local isFocused = Flux.Interact.focused(searchInput)

Hit-Testing Predicates ​

pointIn ​

Flux.Interact.pointIn(x, y, object) returns true if the inset-relative coordinate (x, y) lies within object's AbsolutePosition and AbsoluteSize bounds (inclusive, ignoring rotation):

luau
if not Flux.Interact.pointIn(input.Position.X, input.Position.Y, dropdownMenu) then
    isOpen(false)
end

sunk ​

Flux.Interact.sunk(x, y, object, token?) returns true if input at (x, y) would be claimed by an Active or input-sinking GuiObject rendered above object (or if the Roblox escape menu is open). Pass input as token to memoize the underlying GetGuiObjectsAtPosition query across multiple controls on the same input event:

luau
if not Flux.Interact.sunk(input.Position.X, input.Position.Y, control, input) then
    -- Topmost interactive control at this point
end

covered ​

Flux.Interact.covered(x, y, object, token?) is identical to sunk, except descendants of object do not count as covering it. Use covered on containers (rows, cards, draggable panels) that contain their own buttons:

luau
if not Flux.Interact.covered(input.Position.X, input.Position.Y, cardFrame, input) then
    -- Nothing outside `cardFrame` is covering this point
end