Home › Motion
Spring
Flux.spring(target, frequency?, damping?, initialVelocity?) returns a reactive node driven by damped harmonic motion. When its target changes mid-flight, a spring preserves its momentum and smoothly redirects toward the new goal instead of snapping.
local xPosition = Flux.spring(0, 2, 0.8)
local ui = Flux.new "Frame" {
Size = UDim2.fromOffset(100, 100),
Position = function()
return UDim2.fromOffset(xPosition(), 0)
end,
}
-- Smoothly animate toward 200
xPosition(200)Basic Usage
Flux.spring takes up to four arguments:
target: The initial value (or reactive source) the spring moves toward.frequency(optional, default1): Speed in cycles per second. Higher values move faster.damping(optional, default1): How the spring settles:1(Critically Damped): Quickly approaches the target and stops without overshoot.< 1(Underdamped): Overshoots the target and bounces before settling.> 1(Overdamped): Eases sluggishly into the target without bouncing.
initialVelocity(optional): Starting velocity in the target's type (for example, aVector2for aVector2spring). Omit to start at rest.
A spring acts like a standard Signal. Read it with spring() or :get(), retarget it with spring(newGoal) or :set(newGoal), and bind it directly to Instance properties.
Reactive Targets
Pass a Signal, Computed, or plain function () -> T as target to retarget the spring automatically whenever its dependencies change:
local isHovering = Flux.signal(false)
local buttonSize = Flux.spring(function()
return if isHovering() then UDim2.fromScale(1.1, 1.1) else UDim2.fromScale(1, 1)
end, 3, 0.75)
local myButton = Flux.new "TextButton" {
Size = buttonSize,
Text = "Hover Me",
MouseEnter = function() isHovering(true) end,
MouseLeave = function() isHovering(false) end,
}frequency and damping also accept reactive nodes or functions, so you can retune the spring's physics on the fly.
Velocity Control: Impulse & Set Velocity
Springs expose two methods to manipulate velocity directly without changing the target:
spring:impulse(delta): Addsdelta(in units per second) to the current velocity. Multiple impulses accumulate.spring:setVelocity(velocity): Replaces the current velocity outright.
Both take a value of the spring's datatype and wake a settled spring immediately:
local y = Flux.spring(0, 4, 0.6)
-- Kick upward at 800 px/s, then let the spring settle back to 0
local function onHit()
y:impulse(-800)
end
-- Cancel in-flight momentum
local function onGrab()
y:setVelocity(0)
endSnap & Instant Placement
When you need to reposition a spring immediately without a transition (for example, on initial open or while dragging), use :snap() or :setCurrent():
spring:snap(value): Sets both the target and current animated value tovalueand zeroes velocity, putting the spring at rest immediately.spring:setCurrent(value): Sets the current animated value tovaluewhile preserving the existing target and velocity, waking the spring so it animates fromvalueback toward its target.
local offset = Flux.spring(0, 3, 0.85)
-- Jump directly to 100 at rest (no animation)
offset:snap(100)
-- Displace the current position to 140 so it springs back toward 100
offset:setCurrent(140):impulse(), :setVelocity(), :setCurrent(), and :snap() are spring-only. Calling them on a Tween warns and does nothing.
Lifecycle
When created inside a Scope (such as a component), a spring's per-frame step registration is cleaned up automatically when the scope is destroyed. To stop and dispose a spring early, call spring:Destroy().
A Bound Spring Is Not Owned by Its Instance
Flux.new and Flux.edit own a getter function bound to a property (Position = function() ... end), but a spring passed straight to a property (Position = Flux.spring(fn)) only gets a binding. Created outside any owner, a spring with a reactive target, frequency, or damping keeps its driving Effect subscribed to those sources after the instance is destroyed, and re-runs it whenever they change. Create the spring inside a Scope that is destroyed with the instance, or hand it to the instance's __CLEAN:
local _, button = Flux.scope(function(scope)
return Flux.new "TextButton" {
Size = Flux.spring(function()
return if isHovering() then UDim2.fromScale(1.1, 1.1) else UDim2.fromScale(1, 1)
end, 3, 0.75),
__CLEAN = { scope }, -- destroying `button` destroys the scope, which owns the spring
}
end)Supported Data Types
Flux interpolates almost all standard Roblox UI and 3D data types directly, so you never need to split out X, Y, and Z components or calculate alphas manually.
Supported types include:
number& native LuauvectorUDim&UDim2Vector2&Vector3Color3,ColorSequence&ColorSequenceKeypointNumberRange,NumberSequence&NumberSequenceKeypointCFrame&Rect
Passing an unsupported type, such as a boolean, string, or EnumItem, raises an error. The one exception is nil: a motion that starts at nil warns and then follows its target without animating.
Oklab Color Interpolation
When animating a Color3, ColorSequence, or ColorSequenceKeypoint, Flux converts colors into the Oklab perceptual color space instead of interpolating linear RGB, avoiding muddy or grayish midpoints.
The same perceptual space is available as a standalone toolkit (lighten, saturate, blend, and check contrast) on the Color page.