Motion
What it does
@rat-stack/motion moves values over time in a Foldkit27 app. Its core is four TypeScript modules in packages/motion/src/*.pure.ts. They import only each other, so another project can copy them at a pinned commit.ΒΆ
- Easing: Robert Pennerβs ten curve families come in three variants each: in, out and in-out.ΒΆ
- CSS easing:
linear,cubic-bezier(),steps()and keywords such asease-in-outfollow the CSS spec.ΒΆ - Tween: a value moves between two endpoints over a duration along one easing. The
samplefunction reads the value at any time. A retarget starts a new tween from the sampled value, so the value never jumps.ΒΆ - Spring: a value of any dimension follows a target with stiffness, damping and mass. The
critical(frequency)preset arrives without overshoot.ΒΆ - Interpolation: numbers, tuples, colours in OKLab, and 2D transforms split into translate, scale and rotate.ΒΆ
The spring takes fixed steps of 1/120 s. It carries the leftover time to the next frame. When it comes to rest, it snaps exactly onto the target.ΒΆ
const span = Math.min(MAX_STEP_MS, Math.max(0, elapsed));
const ticks = into.remainder + span / SUBSTEP_MS;
const substeps = Math.floor(ticks + SUBSTEP_GRID_TOLERANCE);
for (let substep = 0; substep < substeps; substep += 1) {
substepInto(into);
if (atRest(into.config, into.position, into.velocity, into.target)) {
return snapInto(into);
}
}
into.remainder = Math.max(0, ticks - substeps);
return into;The Foldkit adapter30, @rat-stack/motion/foldkit, keeps a clock and the named tweens and springs in the Model. A frame is a TickedFrame Message. Motion.advance steps every spring and moves the clock. The view samples each tween at the clock time.ΒΆ
export const motionSubscriptions = <Model, Message>(
options: MotionSubscriptionOptions<Model, Message>
) =>
Subscription.make<Model, Message>()(() => ({
motionFrame: Subscription.animationFrameEntry<Model, Message>({
isActive: options.isAnimating,
toMessage: (elapsed) =>
options.toMessage(Message.TickedFrame({ elapsed })),
}),
reducedMotion: Subscription.persistentEntry(
Dom.streamFromMediaQuery({
mapMatches: (isReducedMotion) =>
options.toMessage(Message.ChangedReducedMotion({ isReducedMotion })),
query: REDUCED_MOTION_QUERY,
})
),
}));Pick a target in the demo at the end of this page, then pick another before the dots arrive. The tween restarts its curve. The spring keeps its speed and turns.ΒΆ
The curve gallery plots every easing.ΒΆ
The standard
- Every easing starts at 0 and ends at 1. The CSS start jumps are the exception:
steps(n, jump-start)begins at 1/n, as the spec requires.ΒΆ - Every curve except back, elastic and bounce never falls. Every out curve mirrors its in curve:
out(t) = 1 - in(1 - t).ΒΆ cubicBezieragrees with an exact bisection reference for any valid control points.ΒΆ- A tween with a monotonic easing stays between its endpoints. A retarget keeps the sampled value.ΒΆ
- One spring step of a span gives the same state as two steps that split it on the substep grid.ΒΆ
- A critically damped spring from rest settles within six periods, snaps exactly, and never overshoots in one dimension.ΒΆ
- Under
prefers-reduced-motion, every tween and spring lands on its target at once.ΒΆ - No Worker60 graph imports the package, so it adds no startup bytes.ΒΆ
Each rule is an it.prop property over generated inputs. Each property also runs against a planted bug, and the suite fails if the property passes it.ΒΆ
"the frame-rate property catches an integrator that takes whole frames",
() =>
falsifies(
Arbitrary.all([
movingSpring,
integerBetween(1, 6),
integerBetween(1, 6),
]),
([from, first, second]) =>
splitAgrees(wholeFrameEuler, from, first, second)
)
);How to check
pnpm --filter @rat-stack/motion testruns the properties and the planted bugs.ΒΆpackages/motion/test/vendoring.test.tsfails if a core module imports anything except another core module, or names a browser global.ΒΆpnpm --filter @rat-stack/site typecheckprints the Mischief startup size against its ceiling.ΒΆ- Open /systems/motion with reduced motion turned on. The dots and curves show their end state.ΒΆ
Sources
- Terms of Use: Easing Functions (Equations). Robert Penner. The MIT and 3-Clause BSD terms for the easing equations; used for the curve set. Accessed 2026-11-02.
- CSS Easing Functions Level 1. W3C. The cubic-bezier() and steps() definitions and the named presets. Accessed 2026-11-02.
- A perceptual color space for image processing. BjΓΆrn Ottosson. The OKLab matrices used for colour interpolation. Accessed 2026-11-02.
Last changed 2026-11-02 in 67d126f. Source on GitHub. Change log.
Linked from
- Agent guide. An Effect stack so pure (aspirational) Kit Langton will blush.
Motion β @rat-stack/motion gives Foldkit apps Penner and CSS easings, tweens, fixed-step springs and interpolation, held in the Model and checked by properties.
- Build and run rat-stack (README.md). What is in the repo, how the example works, and how to run it.
Role: Easing, tweens, springs and interpolation as dependency-free TypeScript, with an optional Foldkit adapter
- Change log. What changed in the files served here, newest first.
Motion β new page: Initial commit from joelhooks/rat-stack template
- Dependency injection is the reason to choose Effect. Effect tracks service requirements in types and supplies replaceable implementations through Layers.
Dillon Mulroy compares Effect with Spring and NestJS:ΒΆ
- Full agent guide. An Effect stack so pure (aspirational) Kit Langton will blush.
- Rat Stack systems | rat-stack. The systems rat-stack ships, the standard each keeps, and how to check it.
Motion β @rat-stack/motion gives Foldkit apps Penner and CSS easings, tweens, fixed-step springs and interpolation, held in the Model and checked by properties.
- Source change history (log.md). The generated change log as Markdown.
Motion β new page: Initial commit from joelhooks/rat-stack template
Spring and tween
Pick a target. Pick another before the dots arrive. The tween restarts its curve from where it is. The spring keeps its speed and bends toward the new target.
β Tween dotβ Spring dot
Curve gallery
Each plot shows one easing over progress from 0 to 1. The dot runs along the curve.
linearquadInquadOutquadInOutcubicIncubicOutcubicInOutquartInquartOutquartInOutquintInquintOutquintInOutsineInsineOutsineInOutexpoInexpoOutexpoInOutcircIncircOutcircInOutbackInbackOutbackInOutelasticInelasticOutelasticInOutbounceInbounceOutbounceInOuteaseease-inease-outease-in-outstep-startstep-end