Animation
Where the motion lives, how reduced motion is honoured, and how presses render without transforms.
1herogpui::theme::set_reduce_motion(true, cx);Timings live in one module — anim.rs — and every component reads them from there. You do not wire animation up per component; composing a modal gets you the modal motion.
Each overlay has its own timing
Each overlay declares its own duration, curve and starting scale. A panel enters over 250ms from 1.05 — a modal settles down onto the page rather than growing into it — while a popover enters over 150ms from 0.90, a list from 0.95, and the backdrop fades alone over 150ms. Panel, popover, list and backdrop exits run 100ms. The drawer slides in over 250ms and leaves over 200ms.
A RenderOnce component leaves the tree the moment its open flag goes false, so an overlay is held for the exit duration first: the phase reports Open, Exiting or Closed, and the component picks the animation from it. That is why a dismissed dialog is still on screen for a frame or two.
Reduced motion
Every animation is gated on one flag, with no opt-in from the caller — a component cannot forget to check it:
1// Anywhere you have an App context.
2herogpui::theme::set_reduce_motion(true, cx);
3herogpui::theme::toggle_reduce_motion(cx);
4
5// Read it if your own view animates.
6if cx.reduce_motion() {
7 // draw the end state directly
8}HeroGPUI keeps its own animation preference, separate from GPUI's App setting. Seed it at startup:
1HEROGPUI_REDUCE_MOTION=1 cargo run -p herogpui-galleryWire this to your own settings
Use HeroGPUI's setter when wiring your application settings so every component observes the same animation preference.Why the press is geometric
GPUI plumbs its transformation matrix into SVG painting alone, so quads and text cannot be scaled or rotated. The scale(0.97) press and the overlay entry zoom are therefore reproduced by changing geometry rather than by transforming: the press scales height, padding, gap, corner radius, minimum width and type size, with margins absorbing exactly what the box gives up, so the outer footprint never changes and pressing a control cannot nudge its neighbour.
Two differences from a real CSS transform remain, and they are visible:
- A label wider than the control's minimum width narrows the control by about 3% of that overflow, because GPUI cannot shrink text without affecting layout.
- An icon child keeps its size through both the press and the overlay zoom, because its dimensions belong to you rather than to the component.
Durations, curves, scales, and which components animate live in anim.rs, next to the components that read them.