Skip to main content
ExodeUI

EXODEUI DESKTOP

A little space for good ideas.

Choose your platform to get the official ExodeUI installer.

Looking for Intel Mac or other builds? Explore the web studio or check all download options.

Prefer the browser? Try for free in your browser →
Tutorial · Interaction3 min read·Updated October 2026·By the ExodeUI team

How to Make UI Animation Interruptible in React

Why CSS transitions jump when a user changes their mind, how springs keep velocity across an interruption, and how to test that your React animation survives rapid input.

Direct Summary & Definition

Core Takeaway

An animation is interruptible when a new user input redirects it from wherever it currently is, keeping its current velocity, instead of restarting from a fixed start value. Springs are interruptible because they describe a target and a physical state; duration-based transitions are not, because they replay a fixed curve from the start. In React, the fix is to drive the value with a spring and read the current value and velocity when the next input arrives.

What actually goes wrong

A CSS transition from A to B has no memory. If the user reverses direction halfway, the element snaps toward A from a curve that assumes it started at rest. The result is a visible jump in speed, and sometimes a visible jump in position if the property is being restarted by a class toggle. The user reads this as the interface ignoring them.

  • Duration-based: fixed start, fixed end, fixed time. Interruption restarts the clock.
  • Spring-based: a target plus current position and velocity. Interruption only changes the target.
  • The tell is a speed change at the moment of input. Position rarely jumps in springs; it jumps constantly in restarted transitions.

The three inputs that get interrupted most

Interruption shows up in predictable places. Fixing these three covers most of the visible jank in a product.

  • Drag then release: the element must continue with the throw velocity, not stop dead at the release point.
  • Toggle tapped twice quickly: the second tap must reverse direction from the current position.
  • Sheet or drawer dismissed mid-open: the close must start from the current offset, not from fully open.

The spring model in one paragraph

A spring is described by mass, stiffness and damping. Stiffness pulls toward the target, damping removes energy, mass resists both. When you change the target mid-flight, you keep the current position and velocity and let those three numbers do the rest. That is the whole trick, and it is why spring libraries feel better than transitions under fast input.

  • Higher stiffness: faster response, more chance of overshoot.
  • Higher damping: less overshoot, slower settle.
  • Critically damped: no overshoot. Use it for menus and sheets where a bounce looks accidental.
  • Underdamped: small overshoot. Use it for playful, low-stakes controls.

Implementing it in React

With Motion, use a spring transition and let the component re-render with a new target; Motion keeps the in-flight velocity. With React Spring, useSpring does the same through its animated values. With plain React state you cannot do this at all, because state changes are discrete: by the time you read the new value the old animation is already gone.

  • Pass the target as state, never as a fixed duration.
  • Do not remount the element on interrupt. Remounting throws away the velocity you need.
  • Keep the spring configuration in one place so every interruptible element feels related.

When a state machine file is the better home

If the interruptible behaviour has to be identical on web and in native apps, or a designer needs to tune the spring without shipping code, the spring belongs in the file rather than the component. ExodeUI stores mass, stiffness and damping per animation, so the same curve plays in React, SwiftUI and Flutter, and React only changes the input that starts or reverses it.

  • Keep the transition rule in the file, not in a React effect that fires on every render.
  • Expose one input per direction rather than a single "open" boolean if reverse needs to keep velocity.
  • Read input names from the file before wiring the React side.

How to test that it is actually interruptible

Manual clicking hides the problem because people rarely click faster than the animation. Test it deliberately.

  • Click or drag twice within 100 ms and watch for a speed discontinuity at the second input.
  • Record the interaction and step through frames; a restart shows as a velocity spike.
  • In Playwright, drive two pointer events with a short gap and compare screenshots at fixed offsets.
  • Check reduced motion: the element should jump directly to the target with no spring at all.

Production Implementation Snippet

Interactive Component Implementation
// Motion: pass a target, not a duration. Velocity is preserved on interrupt.
import { motion } from 'motion/react';

export function Sheet({ open }) {
  return (
    <motion.div
      animate={{ y: open ? 0 : 320 }}
      transition={{ type: 'spring', stiffness: 420, damping: 38, mass: 0.9 }}
    />
  );
}

// ExodeUI: the spring lives in the file; React only changes the input.
import { ExodeUICanvas } from 'exodeui';

<ExodeUICanvas
  src="/sheet.bexode"
  inputs={{ open }}
  fit="Layout"
/>

Frequently Asked Questions

Sources

  1. CSS transition (no velocity carried across a restart) — MDN Web Docs
  2. prefers-reduced-motion — MDN Web Docs
  3. WAI-ARIA Authoring Practices: dialog and sheet patterns — W3C WAI

Related Guides & Examples

Tune the spring once, play it everywhere

ExodeUI stores spring settings in the file, so the same interruptible curve runs in React, SwiftUI and Flutter.

Try the physics playground
Try for free