Frame

01What problem did you find?

Motion gets designed as if it always finishes

01What problem did you find?

Motion gets designed as if it always finishes

People reverse a panel halfway, type while it opens, and cancel a save that has not landed. Tools show the path that finishes, and the interrupted one is usually found by accident in production. So I built Frame around one question: what should a component do when someone changes their mind halfway?

A stylesheet has no good answer. A CSS transition that is turned around starts its easing again from rest, so the edge stops dead and sets off again. The right half of this figure on the project page works that out from the CSS rule, beside a spring that keeps its speed.

02Who was affected?

A frontend engineer tuning one interaction, and so far nobody but me

Frame is for someone who already has a working React component, an inspector or a receipt that unfolds, and wants it to move well when it is interrupted. I wrote a 30 minute task to watch that person do it, and nobody has run it yet.

This is the studio they would sit in. You use the component, and what you did is drawn as a ribbon: the solid line is what happened, the dotted line is the path without the cancel, and dragging the cancel along it moves the component with it.

The Frame studio with the cancel marker dragged along the ribbon. The curve, its dotted path without the cancel, and the expense card above all follow the marker

03What was your hypothesis?

If real use is recorded, the interruption becomes something you can edit

If real use is recorded, then the interruption becomes something you can edit

A keyframe says that at 200 ms the value is 40. An interruption has no fixed time. It lands whenever the person acts, from whatever position and speed the component has reached.

So the bet was to record commands by name while someone uses the real component, and to make the thing you edit a rule for the moment a command lands mid flight: reverse from here, finish first, or ignore it. Here that moment moves from early to late.

04What was your first attempt?

The first design was a keyframe timeline, and it failed on paper

This is the sketch from the design note in the project's first commit, on 6 November 2026. It is familiar and quick to read. It cannot say what happens at an interruption without one track for every moment someone might interrupt, so I never built it.

 [ tracks ]  0ms      200ms      400ms
 panel.y     o---------o----------o
 panel.w     o--------------------o
 body.alpha       o---------------o

05What did later attempts look like, and what was learned?

The take replaced the timeline, and then a hand became one more interruption

05What did later attempts look like, and what was learned?

The take replaced the timeline, and then a hand became one more interruption

The project page that shows the take went through four versions between 6 and 9 November. This first one still carries the numbers from that morning.

By midday it put the expense card on a stage, with the rule and the spring beside it and the take drawn underneath.

On 9 November it gained three components to choose from,

and panels to tune, race and export.

The last change was the largest, and it is the page at the top of this story. The runtime learned to follow a pointer, so you can drag, throw and catch the component itself. It needed no physics of its own, because letting go is an ordinary interruption that starts from the hand's position and speed.

06How did you iterate and test?

The first full check caught 8 of 9 faults, so I threw it out

The main check runs the studio and a clean build of the export through the same four scripts and compares what they draw, in three browser engines. To see whether it would notice a broken export, I wrote 9 faults into clean builds.

8 of 9

seeded faults caught in the first full run

9 of 9

caught in the rerun, with neither clean build flagged

The one it missed was a removed box sizing rule. Removing it changed nothing, because the parts of my test component that exit had no padding. My own protocol said a missed fault invalidates the run, so I gave the fixture padded exits and measured everything again.

A check that cannot fail proves nothing

07What came of it?

Studio and export agree to 0.000023 px, because they are two builds of the same code

07What came of it?

Studio and export agree to 0.000023 px, because they are two builds of the same code

Across 120 scripted runs in Chromium, Firefox and WebKit the two never drew more than 0.000023 px apart. The number is that small because both sides run the same evaluator. It checks the build, the CSS and the replay path, and says nothing about the physics. The physics has its own check against a separate numerical solution, and the worst case there is 0.094 px.

The export adds 7.1 KB of gzipped JavaScript to an app, where Motion adds 45.1 KB for the same interaction and does far more. On main thread time I claim no win over Motion, because the ranges overlapped in one run and not in the next.

08What would you do next?

Next is a person, then a phone

08What would you do next?

Next is a person, then a phone

The task is written: a frontend engineer who has not seen Frame, 30 minutes, six steps.

After that I want to decide whether scrubbing should reflow text to the recorded width, which is the known gap in replay.