# Motion

> For agents: start with the [agent guide](https://ratstack.sh/llms.txt). Every page is Markdown by default; add `Accept: text/html` for HTML.

New on /news: [alchemy-turbo-cache](/news).

## What it does

`@rat-stack/motion` moves values over time in a Foldkit 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 as `ease-in-out` follow the CSS spec.
- **Tween**: a value moves between two endpoints over a duration along one easing. The `sample` function 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.

packages/motion/src/spring.pure.ts at 5fa5df1543b8122a678722d5465ae65fc8a4bf2d; lines 245-259; highlighted 252-254. Title: Each substep checks for rest, so the snap happens at the same instant at any frame rate.

```typescript
  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 [adapter](/lore/hexagonal-architecture), `@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.

packages/motion/src/foldkit.ts at 5fa5df1543b8122a678722d5465ae65fc8a4bf2d; lines 186-202; highlighted none. Title: Frames run only while something moves, and the reduced-motion preference arrives as a Message.

```typescript
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)`.
- `cubicBezier` agrees 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 Worker 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.

packages/motion/test/spring.test.ts at 5fa5df1543b8122a678722d5465ae65fc8a4bf2d; lines 344-355; highlighted none. Title: The frame-rate property must reject an integrator that takes one step per frame.

```typescript
      "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 test` runs the properties and the planted bugs.
- `packages/motion/test/vendoring.test.ts` fails if a core module imports anything except another core module, or names a browser global.
- `pnpm --filter @rat-stack/site typecheck` prints the Mischief startup size against its ceiling.
- Open [/systems/motion](/systems/motion) with reduced motion turned on. The dots and curves show their end state.

## Sources

1. [Terms of Use: Easing Functions (Equations)](https://robertpenner.com/easing_terms_of_use.html)
   Robert Penner. The MIT and 3-Clause BSD terms for the easing equations; used for the curve set. Accessed 2026-11-02.

2. [CSS Easing Functions Level 1](https://www.w3.org/TR/css-easing-1/)
   W3C. The cubic-bezier() and steps() definitions and the named presets. Accessed 2026-11-02.

3. [A perceptual color space for image processing](https://bottosson.github.io/posts/oklab/)
   Björn Ottosson. The OKLab matrices used for colour interpolation. Accessed 2026-11-02.

## Lore on this page

- [CAFE names the Cloudflare, Alchemy, Foldkit and Effect stack](/lore/cafe-stack)

## Linked from

- Agent guide → An Effect stack so pure (aspirational) Kit Langton will blush. → [Read page](https://ratstack.sh/llms.txt)

- Build and run rat-stack (README.md) → What is in the repo, how the example works, and how to run it. → [Read page](https://ratstack.sh/README.md)

- Change log → What changed in the files served here, newest first. → [Read page](https://ratstack.sh/log)

- Dependency injection is the reason to choose Effect → Effect tracks service requirements in types and supplies replaceable implementations through Layers. → [Read page](https://ratstack.sh/lore/dependency-injection-is-the-reason-to-choose-effect)

- Full agent guide → An Effect stack so pure (aspirational) Kit Langton will blush. → [Read page](https://ratstack.sh/llms-full.txt)

- Rat Stack systems | rat-stack → The systems rat-stack ships, the standard each keeps, and how to check it. → [Read page](https://ratstack.sh/systems)

- Source change history (log.md) → The generated change log as Markdown. → [Read page](https://ratstack.sh/log.md)
