Skip to content

Guide Animation

MeowLynxSea edited this page Jun 18, 2026 · 5 revisions

Animation

Every stateful composite (SelectState, ModalState, PopoverState, TooltipState, DropdownMenuState, ComboBoxState) owns an AnimatedVisibility field. The renderer reads its progress; the orchestrator drives the timer.

You don't usually write animation code yourself. Just call state.update(cx, |s, _| s.open()) / close() / toggle() — the renderer handles the rest.

The lifecycle

state.update(cx, |s, _| s.open())     // user clicks trigger
  → AnimatedVisibility::show()       // target = true
  → orchestrator advances progress 0 → 1 over enter_config.duration
  → renderer paints enter animation (fade / slide / scale)

state.update(cx, |s, _| s.close())    // user picks / presses Escape
  → AnimatedVisibility::hide()       // target = false
  → orchestrator advances progress 1 → 0 over exit_config.duration
  → renderer paints exit animation

A composite stays mounted while is_visible() is true, even during the exit phase — the renderer paints the fading-out visual until progress reaches 0.

Composite state methods

Composite Methods
Select .open() / .close() / .toggle()
ComboBox .open() / .close() / .toggle()
Modal .open() / .close() + .invoke_close(reason, window, cx)
Popover .open() / .close() / .toggle()
DropdownMenu .open() / .close() / .toggle()
Tooltip .open() / .close() + .set_delay_ms(ms)

ModalCloseReason is Escape / ScrimClick / Programmatic.

Example

let state: gpui::Entity<SelectState> = SelectState::new(&mut *cx);
let state_for_btn = state.clone();

button("select-trigger", cx)
    .on_click(move |_, _, cx| state_for_btn.update(cx, |s, _| s.toggle()))
    .render(cx)
    .child("Pick…");

select("select-id", state).render(cx)

Reading the theme for durations

Renderers read tokens.motion.duration_normal and tokens.motion.easing_standard from the theme so theme switching retimes every animation.

AnimationConfig

For custom painter elements, use AnimationConfig directly:

use std::time::Duration;
use yororen_ui::animation::{AnimationConfig, ease_out_cubic};

let cfg = AnimationConfig::from_motion(Duration::from_millis(200), ease_out_cubic);

Presets are also available: FadeIn / FadeOut, ScaleIn / ScaleOut, fade_slide_in_right, bounce_in_*, pulse, etc. Theme tokens are the source of truth in real apps; presets stay convenient for prototyping.

Manual animation (custom gpui::Element)

For a one-off animation that doesn't fit a composite, use gpui's with_animation directly:

div.with_animation(
    "card-enter",
    Animation::new(Duration::from_millis(200)).with_easing(ease_out_cubic),
    fade_slide_in_right(px(16.0)),
)

The MaterialRippleElement in crates/yororen-ui-demos/layers_demo/src/material_button.rs is the canonical example of a hand-rolled element using request_animation_frame to advance its own f32 progress.

Best practices

  1. Default to AnimatedVisibility. Every composite already has it.
  2. Use tokens.motion.* for durations so theme switching retimes animations.
  3. Keep animations short. 100–300 ms covers almost every case.
  4. Pick easing by intent: ease_out_* for entrance, ease_in_* for exit, ease_in_out_* for equal emphasis, ease_out_back for attention.
  5. Clamp overshoot easings with the *_clamped variants when passing to gpui's Animation::with_easing.
  6. Don't run side effects in render. Spawning animation timers goes in event handlers.

See also

Clone this wiki locally