Skip to content

Motion

gpui-base owns deterministic motion sampling and lifecycle while leaving every visual choice to the application. It provides stable keyed state, interruption, reversal, animation-frame requests, and reduced-motion behavior without imposing product timing or styling.

Run the interactive companion for this guide:

bash
cargo run -p gpui-base --example motion

The example contains five separate demos. Use the tabs at the top to inspect one capability at a time.

Capability map

DemoAPIWhat it demonstrates
Sliding timetransitionFour independently rolling digits, 08:00–20:00, with targets changing faster than the transition settles
SpringspringA segmented-control indicator that preserves velocity when rapidly retargeted
KeyframesKeyframes, Timing, animate_keyframesA repeating multi-stop activity signal
StaggerStaggerAllocation-free timing offsets across a list
PresencePresenceExit animation that keeps content mounted until it becomes absent

The library also exposes Easing, Discrete, MotionTransform, and MotionReveal. They compose with the same primitives rather than requiring separate animation runtimes.

Target transitions

Use transition for a value moving toward a target over a known duration. Every independently animated value needs a stable ID.

rust
let opacity = transition(
    ("save-dialog", "opacity"),
    if open { 1.0 } else { 0.0 },
    Transition::new(Duration::from_millis(180)).easing(Easing::EaseOut),
    window,
    cx,
);

Retargeting starts at the currently sampled value. Direct reversal shortens the return duration, so reversing early does not spend a full duration retracing a short distance. transition_with_status additionally returns Idle, Delayed, Running, or Finished.

Easing includes CSS keyword curves, cubic Bézier curves, all CSS step positions, and piecewise linear() stops. Invalid parameters return typed errors.

Springs

Use spring when the target may change while moving. It preserves both position and velocity, which makes it suitable for selection indicators and settling spatial values.

rust
let x = spring(
    "selected-indicator",
    selected_x,
    Spring::new(Duration::from_millis(420)).with_damping(0.72),
    window,
    cx,
);

Do not make a pointer-controlled value chase the pointer through a spring. Set with_travel(false) during direct manipulation and restore travel after release.

with_damping requires a finite, non-negative ratio; with_epsilon requires a finite value greater than zero and interprets it in the target's own units. The builders panic for invalid trusted constants. Use try_with_damping and try_with_epsilon for configuration or user-provided values. Normalized values normally keep the 0.001 default; pixel motion can use a coarser tolerance such as 0.1.

Keyframes and timing

Keyframes describes validated value stops. Timing uses absolute elapsed time and supports signed delays, finite or infinite iterations, and normal, reverse, or alternating playback.

rust
let frames = Keyframes::try_new([
    Keyframe::new(0.0, 0.25),
    Keyframe::new(0.45, 1.0).ease(Easing::EaseOut),
    Keyframe::new(1.0, 0.25),
])?;

let opacity = animate_keyframes(
    "activity",
    &frames,
    Timing::new(Duration::from_millis(1400))
        .iterations(IterationCount::Infinite),
    window,
    cx,
).value;

Offsets must start at 0, end at 1, and be monotonic. Use Discrete when a value cannot be interpolated.

animate_keyframes retains its playback start time under the supplied stable ID. Re-rendering with the same ID continues the current sequence. To replay it, include an application-owned generation in the ID, such as ("notification-enter", generation), and increment that generation for each replay.

Presence and stagger

Presence separates logical visibility from physical mounting. Its phases are entering, present, exiting, and absent. Render while should_render() is true and use progress for the chosen visual properties. Reopening during exit reverses from the current sample.

Stagger calculates a delay for an index from the first, last, center, or a chosen origin. It does not allocate a schedule or own list identity:

rust
let stagger = Stagger::new(Duration::from_millis(80), StaggerOrigin::First);
let delay = stagger.delay(index, item_count);

Measured reveal

MotionReveal measures a child at its natural size and clips its visible height by progress. Collapsible::motion_id(...) is the convenient control-level facade. Without a motion ID, the control keeps immediate mount/unmount behavior.

Reduced motion and performance

Transitions, springs, keyframes, presence, and reveal-compatible controls honor GPUI's reduced-motion preference. Finite motion snaps to the target, synchronizes retained state, and leaves no pending animation frame. Motion must never be the only way state is communicated.

The pure steady sampling paths measured by the benchmark—timing/easing, keyframe lookup, analytic spring integration, and stagger delay calculation—are allocation-free. Keyed transition, spring, presence, and reveal lifecycles are covered by GPUI retained-state and frame-request tests because those updates belong to the framework lifecycle rather than the pure sampler. Sampling uses absolute elapsed time, and keyframe lookup uses binary search. Run the release benchmark with:

bash
cargo bench -p gpui-base --bench motion

Choose the smallest suitable primitive: transition for duration-based targets, spring for changing spatial targets, keyframes for authored sequences, Presence for exit-before-unmount, and Stagger for list choreography.

Benchmark results

Measured on Linux x86_64 with a release build, 31 batches, and 200 iterations per batch:

WorkloadMedianP95WorstAllocations
1,000 scalar timing + easing samples26.490 µs26.567 µs27.290 µs0
1,000 keyframe samples, 2 frames21.656 µs21.707 µs21.729 µs0
1,000 keyframe samples, 8 frames25.197 µs25.251 µs25.269 µs0
1,000 keyframe samples, 32 frames27.932 µs27.969 µs27.971 µs0
1,000 analytic spring integration samples6.042 µs6.106 µs6.216 µs0
1,000 stagger delay calculations0.574 µs0.583 µs0.587 µs0

The scalar timing/easing workload remains below its 100 µs median budget. These figures are a reproducible development baseline rather than a cross-platform guarantee; run the benchmark on each target platform when platform-specific performance matters.