05

Interruptibility

Animation that can change its mind.

Users change their mind. They open a sheet and close it before it lands. They hover, leave, and hover again. Interruptibility is what happens to your animation in that moment: does it pick up from where it is, or does it start over?

Both sheets open together. Before they land, the open runs again. Press Open again to catch it yourself. The travel is slowed down so the restart is easy to see.

Keyframes are on the left. A keyframe animation has a fixed start and end, so the second open restarts from the first frame. The sheet teleports to the bottom and climbs again. The transition is on the right. It animates from the current value to the new target, wherever the sheet happens to be. Nothing jumps.

This is the whole rule. Keyframes describe a fixed clip. Transitions describe a destination. Anything a user can toggle wants a destination.

Springs keep their momentum

A transition fixes the jump, but a tween still stops dead and restarts its easing curve when you change the target. It goes from full speed to a fresh ease-in. A spring carries its velocity into the new target, so the reversal reads as one motion instead of two.

Each dot turns around before it arrives. The tween starts a new ease. The spring keeps its speed.

The difference is subtle at slow speeds and large at fast ones. For anything a user can flick back and forth, a switch, a sidebar, a drag handle, a spring is the safer default.

Layout that keeps moving

Interruptibility is not only about reversing. When new elements arrive while others are still animating, every element in flight needs a new target. A toast stack is the classic case: hit the button quickly and each toast slides up to make room while the previous one is still settling.

    Motion's layout prop retargets each toast as siblings enter and leave, so a click that lands mid-animation joins the motion instead of resetting it.

    Usage

    A transition is enough for most toggles. Reach for keyframes only when the animation is a one-shot that nothing can interrupt, like a loading spinner.

    .sheet {
      transform: translateY(100%);
      transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1);
    }
    
    .sheet[data-open] {
      transform: translateY(0);
    }
    /* Restarts from the first frame every time it is applied. */
    @keyframes slide-up {
      from { transform: translateY(100%); }
      to { transform: translateY(0); }
    }
    
    .sheet[data-open] {
      animation: slide-up 300ms cubic-bezier(0.23, 1, 0.32, 1) both;
    }
    import { motion } from "motion/react";
    
    <motion.div
      animate={{ y: open ? 0 : "100%" }}
      transition={{ type: "spring", stiffness: 300, damping: 30 }}
    />

    A quick test for any component you ship: open it and close it as fast as you can, ten times. If anything flashes, jumps, or finishes the old animation before starting the new one, it is not interruptible yet.