Back to scenario guides
Scenario guide 04

Choose spring or ease-out by meaning

Both can bring an interface to rest. Springs retain force and interruption; ease-out suits a deliberate, restrained arrival.

The choice between spring and ease-out begins with the action’s meaning. Directly manipulated, interruptible objects benefit from continuous velocity and a sense of force. Clear confirmation, entrance, and result handoff benefit from a predictable arrival. Compare both rhythms on the same state and the interface becomes easier to read.

Make this decision first

Ask whether the object can be dragged, interrupted, or reversed, then decide whether it needs physicality or a crisp endpoint.

Implementation path

  1. 01

    Use spring for continuous input

    Drags, drawers, and directly manipulated objects benefit when velocity can carry into the next input.

  2. 02

    Use ease-out for clear feedback

    Saving, copying, and light entrances should arrive quickly; overshoot can pull focus from the information.

  3. 03

    Compare the same state

    Hold travel, size, and duration close, then change only the curve to see the difference in velocity.

Decide what the action is explaining

Curve names can pull discussion toward visual preference, while people actually feel how an object answers their input. Write one responsibility for the action first: a sidebar returns to its dock after following a drag, a card communicates current focus after selection, a Save button confirms that work was received, or a menu arrives near its trigger in a readable position. Different responsibilities give speed curves different meaning. An object continuously affected by a gesture, likely to be grabbed again, or likely to reverse direction benefits from continuity between current position and velocity. A one-time confirmation, brief entrance, or result that should quickly release attention benefits from a clear restrained endpoint. This judgment is more reliable than asking for a Bézier curve first.

State identity is a second decision point. The same drawer moves from closed to open, so people can track one piece of content in space and it can settle with a sense of weight. A button before and after Save occupies the same place, yet its meaning changes from available to working to complete. Reading its copy and outcome is the priority, and overshoot can compete for attention. Cards in a reordered list retain identity while a large amount of information needs reading, so let real position stabilize first and add only a very small emphasis. Put object identity, user input, and final reading goal in the design note. Design, product, and engineering can then choose a curve from the same reasons.

Direct manipulation and one-time confirmation branch from action meaning to spring and ease-out, labeled with continuity and clear arrival.Continuous inputClear outcomeActionmeaningObject,input,outcomeDirectmanipulationDrag, pull,moveSpringRetainvelocity andinterruptionOne-timeconfirmationSave, copy,menuEase-outArrivequickly andclearly
Diagram 1: choose a velocity model from action meaningDirect manipulation and one-time confirmation branch from action meaning to spring and ease-out, labeled with continuity and clear arrival.

Use springs to retain velocity across continuous input

A spring works well when an object is understood as something being pushed. Dragging a card, pulling open a drawer, adjusting a bottom panel, scrubbing media, and moving a canvas element already provide direction, distance, and velocity through the person’s gesture. After release, a spring can carry that information toward a target so the object settles naturally. When the next touch or pointer input arrives, it can take over from the current velocity. That continuity makes the interface feel responsive to the person instead of restarting from a prerecorded segment on every interaction. A spring serves control, so target position, velocity transfer, and cancellation belong in the same component contract as gesture state.

Parameter tuning can begin with three relationships. Stiffness decides how strongly the target pulls the object back, so a higher value reaches the destination faster. Damping decides how velocity is absorbed, so a higher value settles rebound sooner. Mass shapes response rhythm under the same force. Fix travel and target first, then change one dimension at a time and observe whether the object still feels controllable. A navigation drawer usually needs higher damping and a compact settle so it does not keep moving while covering content. A draggable card can allow one soft small rebound to confirm the drop. Good parameters depend on consequence, frequency, and screen density, so each component should keep a reasonable range instead of copying one global set of numbers.

Use ease-out for a quick, clear arrival

The value of ease-out is useful speed at the beginning followed by a gradual slowdown before arrival. People first see that change has happened, then have time to read the final state. It suits menus, notices, light card entrances, copy confirmation, save outcomes, and a button recovering from press. These actions usually have a clear start and end and rarely need to inherit velocity from a previous gesture. Keep duration within the needs of content and task and the visual response feels decisive. Button feedback often settles within 100 to 180 ms, local state handoff can run a little longer, and a complex layer entrance can grow with reading load. Every part of the time should help someone read a result or find the next step.

An ease-out animation also needs to protect spatial relationship. When a menu appears near its trigger, starting opacity, very small travel, and a stable final boundary are enough to explain origin. When a success notice fades beside Save, copy, state color, and icon express completion together. Large scale, rotation, and repeated bouncing can make a simple result more prominent than the content itself. Applying the curve to opacity and transform keeps layout stable and makes reduced-motion mode easy to shorten or hand off directly. Ease-out acts as an information-ordering tool here: acknowledge the action, present the outcome, then return the person to the next operation.

Stiffness, damping, and mass connect to arrival speed, rebound settling, and response rhythm, then converge on a controllable landing.StiffnessShapesarrival speedDampingShapesreboundsettlingMassShapesresponserhythmControllablelandingAcomponent-specificreasonablerange
Diagram 2: how spring parameters shape settlingStiffness, damping, and mass connect to arrival speed, rebound settling, and response rhythm, then converge on a controllable landing.

Interruption, reversal, and repeated input determine implementation

Curve choice depends on interruption strategy. A person may pull a drawer back while it is halfway open, move another card while reorder motion is still settling, or edit a field again before a save outcome appears. The component needs to know what remains when new input arrives: current visual position, current velocity, target state, focus, and accessibility copy. A directly manipulated spring should cancel its old target immediately and take over from the current value. A one-time ease-out response can yield to new state so the old outcome fades or finishes directly. In every path, state source should lead timers, preventing a stale callback from repainting an interface after intent has changed.

Browser and animation-library APIs also shape handoff quality. CSS transition suits concise, predictable ease-out state changes. The Web Animations API provides cancellation, completion, and play state for short actions managed by a state machine. A physical spring commonly needs per-frame integration or a library velocity model. When choosing implementation, check whether the component receives drag velocity, reverses at any moment, or has multiple objects sharing one state. Keep cancellation functions, target values, and completion conditions explicit so tests can cover fast repeat presses, direction reversal, and route change. With implementation made clear, the curve retains its intended meaning in real use.

Compare on the same state and complete review

When comparing spring and ease-out, lock travel, object size, trigger position, and final state, then change only the velocity model. The team can then observe the real difference: does the spring make continuous control feel more natural, and does ease-out make the outcome easier to read? If distance, color, shadow, and duration change at the same time, discussion quickly loses causal connection. Keep a switchable comparison in component documentation and show a complete product moment, such as pulling open a drawer before selecting navigation or saving before seeing confirmation and continuing to edit. The feel of an atomic motion earns its place when the full flow tests whether it helps someone understand the current action.

Review also covers reduced motion and accessibility. With the system preference enabled, a sidebar still reaches the correct position, focus still enters an operable region, and save state still moves from Saving to Saved. Reduce broad travel, repeated rebound, and decorative rotation. Check keyboard triggering, touch drag, overlap on narrow screens, continuous input, and cancellation during page exit. Finally record why each component chose its curve: what meaning the action serves, how much rebound it allows, its maximum duration, how it interrupts, and what the low-motion path retains. This record keeps future design consistent and gives people a predictable motion language across pages.

Gesture input, current visual value, target state, and completed outcome form a loop; new input returns to gesture input and cancels the old target.New inputGesture inputDirection andvelocityCurrentvisual valuePosition andfocusTarget stateOpen, closed,savedCompletedoutcomeReadable andactionableCancel oldtargetNew intenttakes over
Diagram 3: retain one state contract through interruptionGesture input, current visual value, target state, and completed outcome form a loop; new input returns to gesture input and cancels the old target.

Spring and ease-out selection checklist

  • Write the meaning the action must explainState object identity, user input, final reading goal, and the next action.
  • Direct manipulation retains position and velocity continuityDrags, drawers, and canvas objects have a strategy for target, cancellation, and new input takeover.
  • Clear outcomes use a compact arrivalMenus, save, and copy confirmation use short ease-out to support reading and continued work.
  • Change only the velocity model during comparisonLock travel, size, final state, and trigger position so the difference can be observed directly.
  • Interruption and the low-motion path have been reviewedCover repeat presses, reversal, keyboard, touch, route exit, and system-preference changes.

Case: a drawer uses spring after drag while save feedback uses ease-out

After drag release, a drawer retains velocity and settles open or closed; save feedback reaches a clear completed state near the original action.

<section class="curve-demo">
  <label>
    Drawer position
    <input type="range" min="-240" max="0" value="-240" data-drawer-range>
  </label>
  <button type="button" data-drawer-toggle>Open drawer</button>
  <div class="drawer-stage">
    <aside class="drawer" data-drawer aria-label="Settings drawer">Settings stay attached to the drag position.</aside>
  </div>
  <button type="button" data-save>Save settings</button>
  <p class="save-status" data-save-status data-state="idle" role="status" aria-live="polite">Ready to save.</p>
</section>

<style>
.curve-demo { display: grid; gap: 12px; max-width: 32rem; }
.drawer-stage { overflow: hidden; min-height: 4rem; border: 1px solid currentColor; }
.drawer { width: 14rem; min-height: 4rem; padding: 12px; background: Canvas; }
.save-status { opacity: 0; transform: translateY(6px); transition: opacity 160ms ease-out, transform 160ms ease-out; }
.save-status[data-state="saving"], .save-status[data-state="saved"] { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .save-status { transition-duration: 1ms; }
}
</style>

<script>
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const drawer = document.querySelector("[data-drawer]");
const drawerRange = document.querySelector("[data-drawer-range]");
const drawerToggle = document.querySelector("[data-drawer-toggle]");
const saveButton = document.querySelector("[data-save]");
const saveStatus = document.querySelector("[data-save-status]");
let drawerPosition = -240;
let drawerVelocity = 0;
let drawerTarget = -240;
let lastInputTime = performance.now();
let cancelDrawerSpring = () => {};

function springTo(element, from, target, releaseVelocity = 0, onUpdate = () => {}) {
  let position = from;
  let velocity = releaseVelocity;
  let previous = performance.now();
  let frame = 0;
  let finished = false;
  const stiffness = 340;
  const damping = 34;
  const update = (nextPosition, nextVelocity) => {
    position = nextPosition;
    velocity = nextVelocity;
    element.style.transform = "translateX(" + position + "px)";
    onUpdate(position, velocity);
  };
  const cleanup = () => {
    reducedMotionQuery.removeEventListener("change", onReducedMotionChange);
  };
  function onReducedMotionChange(event) {
    if (event.matches) finish();
  }
  const finish = () => {
    if (finished) return;
    finished = true;
    window.cancelAnimationFrame(frame);
    update(target, 0);
    cleanup();
  };
  const cancel = () => {
    if (finished) return;
    finished = true;
    window.cancelAnimationFrame(frame);
    onUpdate(position, velocity);
    cleanup();
  };

  if (reducedMotionQuery.matches) {
    finish();
    return () => {};
  }

  reducedMotionQuery.addEventListener("change", onReducedMotionChange);
  const tick = (now) => {
    if (finished) return;
    const dt = Math.min((now - previous) / 1000, 0.032);
    previous = now;
    velocity += (-stiffness * (position - target) - damping * velocity) * dt;
    position += velocity * dt;
    update(position, velocity);
    if (Math.abs(velocity) < 0.1 && Math.abs(position - target) < 0.1) {
      finish();
      return;
    }
    frame = window.requestAnimationFrame(tick);
  };

  frame = window.requestAnimationFrame(tick);
  return cancel;
}

function setDrawerPosition(position, velocity = 0) {
  drawerPosition = Math.max(-240, Math.min(0, position));
  drawerVelocity = velocity;
  drawer.style.transform = "translateX(" + drawerPosition + "px)";
  drawerRange.value = String(Math.round(drawerPosition));
}

function settleDrawer(target = drawerPosition > -120 ? 0 : -240) {
  cancelDrawerSpring();
  drawerTarget = target;
  drawerToggle.textContent = target === 0 ? "Close drawer" : "Open drawer";
  cancelDrawerSpring = springTo(drawer, drawerPosition, target, drawerVelocity, (position, velocity) => {
    drawerPosition = position;
    drawerVelocity = velocity;
    drawerRange.value = String(Math.round(Math.max(-240, Math.min(0, position))));
  });
}

drawerRange.addEventListener("input", () => {
  cancelDrawerSpring();
  const now = performance.now();
  const nextPosition = Number(drawerRange.value);
  const nextVelocity = (nextPosition - drawerPosition) / Math.max((now - lastInputTime) / 1000, 0.016);
  lastInputTime = now;
  drawerTarget = nextPosition > -120 ? 0 : -240;
  drawerToggle.textContent = drawerTarget === 0 ? "Close drawer" : "Open drawer";
  setDrawerPosition(nextPosition, nextVelocity);
});
drawerRange.addEventListener("pointerdown", () => {
  cancelDrawerSpring();
  lastInputTime = performance.now();
});
drawerRange.addEventListener("change", () => settleDrawer());
drawerToggle.addEventListener("click", () => {
  settleDrawer(drawerTarget === 0 ? -240 : 0);
});

async function saveSettings() {
  if (saveButton.disabled) return;
  saveButton.disabled = true;
  saveStatus.dataset.state = "saving";
  saveStatus.textContent = "Saving settings.";
  await new Promise((resolve) => window.setTimeout(resolve, 160));
  saveStatus.dataset.state = "saved";
  saveStatus.textContent = "Settings saved.";
  saveButton.disabled = false;
}

saveButton.addEventListener("click", saveSettings);
setDrawerPosition(drawerPosition);
</script>

`springTo` receives releaseVelocity from the instant of release and uses it as the integrator’s initial velocity, so a new drag, reversal, or route exit can take over immediately. A reduced-motion preference change lands at the target through its change event, with listeners cleaned up on completion and cancellation; save feedback uses a short ease-out while copy and state remain driven by the same business state, letting people read completion and continue their work.