Design motion, transitions, and bespoke graphics for native Rust gpui apps — the framework behind Zed and the pd-console operator console. Use for with_animation, easing curves, BoxShadow/glow, breathing dots, pane expand/zoom/slide transitions, and custom paint/Vello/wgpu surfaces. Trigger on: gpui animation, with_animation, gpui transition, gpui easing, BoxShadow, gpui shadow, pd-console motion, gpui paint/canvas, Vello, wgpu, "lift/slide/zoom/spring in gpui", reduced-motion in a native Rust UI, "animation re-renders forever", repeat() never stops. NOT for: web/React motion (→ animation-system-architect), CLI/TUI output (→ beautiful-cli-design), general non-motion GUI layout/color/typography (→ beautiful-gui-design).
Installation
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Design motion, transitions, and bespoke graphics for native Rust gpui apps — the framework behind Zed and the pd-console operator console. Use for with_animation, easing curves, BoxShadow/glow, breathing dots, pane expand/zoom/slide transitions, and custom paint/Vello/wgpu surfaces. Trigger on: gpui animation, with_animation, gpui transition, gpui easing, BoxShadow, gpui shadow, pd-console motion, gpui paint/canvas, Vello, wgpu, "lift/slide/zoom/spring in gpui", reduced-motion in a native Rust UI, "animation re-renders forever", repeat() never stops. NOT for: web/React motion (→ animation-system-architect), CLI/TUI output (→ beautiful-cli-design), general non-motion GUI layout/color/typography (→ beautiful-gui-design).
license
Apache-2.0
allowed-tools
Read,Write,Edit,Bash(cargo *)
metadata
{"category":"Frontend & UI","tags":["gpui","rust","animation","motion","transitions","bespoke-graphics","vello","wgpu","pd-console"],"pairs-with":[{"skill":"beautiful-gui-design","reason":"Motion serves a design system — establish hierarchy, color, and typography before animating them."},{"skill":"animation-system-architect","reason":"Same motion-architecture discipline (one owner, reduced-motion-first, compositor-friendly) ported from web to native gpui."}],"runtime":{"argument-hint":"[pd-console-root] [surface: micro|transition|bespoke]","mcp-tools":[]},"extra":{},"authorship":{"maintainers":["some-claude-skills"]}}
Rust gpui Motion
Motion in gpui is not CSS with a different syntax. gpui 0.2.x has no fluent transform on div — no .scale(), no .translate(), no .rotate(), no transform property to interpolate, no spring on layout, no AnimatePresence, no compositor thread to offload to. Every "lift / slide / zoom / spring" you internalized from Framer Motion and View Transitions must be re-derived from the four primitives gpui actually ships: opacity, BoxShadow, hover/color interpolation, and animated layout fractions driven by the delta closure inside with_animation. A .repeat() animation is a standing invitation to re-render the whole window forever, and every re-render walks the entire element tree top-to-bottom — so the frame budget here is an architecture concern, not a styling concern. This skill is the field manual for doing that right in pd-console (native gpui on Metal). It is the gpui counterpart to the web-focused animation-system-architect.
Activation Triggers
Activate on:with_animation, Animation::new, gpui easing (ease_in_out, bounce, pulsating_between, linear), BoxShadow/glow, breathing/pulsing dots, pane expand / list reflow / tab-slide / fullscreen-zoom transitions, "lift/slide/zoom/spring in gpui", custom paint/canvas(), PathBuilder, Vello/wgpu surfaces, "animation never stops re-rendering", reduced-motion in a native Rust app, two owners fighting one element.
NOT for: web/React/Framer/GSAP/View-Transitions motion → animation-system-architect | terminal/CLI/TUI output → beautiful-cli-design | general GUI layout, color, theming, typography (the non-motion design system) → beautiful-gui-design.
Quick Start
Classify the surface — a micro-interaction (glow, breathing dot, hover lift), a transition (a surface changes shape or identity), or bespoke graphics (the element tree genuinely can't express it). Different primitives, different owners.
Gate on reduced-motion and frame budget FIRST. There is no @media fallback and no compositor thread. Decide the static/instant path before the animated one.
Animate compositor-friendly properties only — opacity, BoxShadow/glow, color; never width/height/px/insets in a hot render. Layout animation costs a full tree walk per frame.
One motion owner per surface. A transition state machine lives in the view and is retargetable mid-flight. Never let two with_animations or a repeat() and a state machine drive the same element.
Audit the rendered frame, not the snippet. Build it, run it, watch for runaway re-renders (.repeat()), label overlap, and motion that outlives its job.
Drop here only when div().bg().shadow().child() truly can't express it (dither, shimmer, particles, beziers).
Heaviest surfaces
Vello / wgpu surface (ADR-0086)
Custom-render path for scanlines/playheads/particle fields; highest cost, last resort.
Decision Points
flowchart TD
A[Need motion on a gpui surface] --> B{Reduced motion requested?}
B -->|Yes| C[Resolve to final state instantly; keep orientation cue, drop travel]
B -->|No| D{Does the surface change SHAPE or IDENTITY?}
D -->|"No — atom: glow, lift, breathing, hover"| E{Loops forever?}
E -->|"Yes — life/pulse"| F["Animation::new(..).repeat()<br/>pulsating_between — ONE owner, budgeted"]
E -->|"No — one-shot ack"| G["with_animation: map delta→opacity/BoxShadow"]
D -->|"Yes — pane expand / list reflow / tab slide / fullscreen zoom"| H{Can the element tree express it?<br/>opacity + shadow + layout-fraction}
H -->|Yes| I["Interruptible transition state machine in the view<br/>retarget mid-flight, one owner"]
H -->|"No — dither / shimmer / particles / beziers / scanlines"| J{How heavy?}
J -->|"Path / quad / shader-ish in one frame"| K["gpui paint API: canvas(), paint_quad, PathBuilder"]
J -->|"Continuous custom-rendered surface"| L["Vello / wgpu surface (ADR-0086) — last resort"]
Expert cue: if you reach for "I just need transform: scale()", stop — gpui has no such thing. Re-derive the intent ("emphasize this pane") as opacity + BoxShadow growth + an animated layout fraction. The transform vocabulary does not port; the intent does.
Core Rules
No-transform constraint is law. There is no scale/translate/rotate to interpolate. Compose every "lift/slide/zoom/spring" from opacity, BoxShadow, color, and the delta closure. (references/02-the-no-transform-constraint.md is the full translation table.)
Compositor-friendly only in a hot render. Animate opacity, BoxShadow, and color. Animating width/height/insets/px forces a full element-tree re-layout every frame. If you must animate layout, animate a fraction with eyes open, never as a convenience default.
One owner per surface. Exactly one thing drives a given element's motion — one with_animation, or one transition state machine. Two owners fighting one element is the native equivalent of the web ownership conflict and produces irreproducible snapping.
Reduced-motion-first, and it must preserve orientation. Decide the instant/static path before the animated one. Reducing motion means less travel, not deleted feedback — keep the orientation cue (final position, a fade, a state hint), drop only the journey.
Frame budget is architecture, not polish. A .repeat() animation re-renders the window forever; every re-render walks the whole tree. Budget loops deliberately, scope them to the smallest view, and never leave one running for a surface that's off-screen or idle.
Transitions are interruptible state machines that live in the view. A surface that changes shape/identity gets a retargetable state, not a fire-and-forget keyframe. Mid-flight interruptions retarget the current value; they don't restart from zero.
Drop below the widget layer only when the tree can't express it.paint/canvas/Vello are escape hatches with real cost. Earn them; don't default to them for things div().shadow() already does.
Failure Modes
Layout animation in a hot render
Symptom: A pane "expands" by animating width/height (or px(...) insets) and the whole window stutters as more content loads.
Detection:delta closure writes to size/inset fields inside a frequently-rendered view; frame time climbs with tree depth, not with the animated element.
Fix: Animate opacity + BoxShadow growth for the "lift", and if shape must change, animate a single layout fraction at the smallest enclosing node — not raw pixels on every child.
repeat() that never stops re-rendering
Symptom: CPU/GPU stays hot on an idle screen; the window re-renders continuously even with no input.
Detection: An Animation::new(..).repeat() (e.g. a breathing dot) mounted high in the tree or left running while its view is off-screen/inactive.
Fix: Scope the loop to the smallest leaf view that needs it; pause/unmount it when the surface is idle or hidden; never stack two loops on one element.
Reduced-motion deletes orientation
Symptom: With reduced motion on, a transition just snaps and the user loses track of what moved where (which pane expanded, which tab is now active).
Detection: The reduced-motion branch sets duration to 0 / removes the animation entirely with no replacement cue.
Fix: Replace travel with a minimal fade or an explicit final-state hint. Reduced motion = smaller motion that still communicates, not silence.
Two owners fighting one element
Symptom: Flicker, value snapping, stale shadow/opacity, "impossible" bugs only after an interrupted interaction.
Detection: Both a with_animation and a state-machine field (or two with_animations) write the same property on the same element.
Fix: Make one owner authoritative. Let the transition state machine own the value and have everything else read it; remove the competing with_animation.
Reaching below the widget layer too early
Symptom: A hand-rolled paint/Vello surface re-implements a glow or a rounded card that BoxShadow + div already does, now untestable and off the layout grid.
Detection: Custom paint code whose output is expressible as div().bg().shadow().rounded().
Fix: Return to the element tree. Reserve paint/Vello for genuinely inexpressible work (dither, shimmer sweep, particles, anti-aliased causal-thread beziers, scanlines).
Easing that doesn't exist
Symptom: Build fails or motion falls back to linear because you reached for a web curve name.
Detection:ease_out (not exported in 0.2.x) or other CSS-named curves in with_easing(..).
Fix: Use confirmed gpui curves: ease_in_out, bounce, pulsating_between, linear. For deceleration, ease_in_out reads like a heavy door settling on a good hinge — that's the "buttery" target.
Worked Example — pd-console spawn → fullscreen → diff, done right
Goal. An operator spawns an agent; the spawn card expands to advanced view, the fleet renders as a list, the selected pane zooms to fullscreen, and the diff slides in as a tab. Buttery, interruptible, reduced-motion-safe — with zero transform usage.
One owner. A single TransitionState field on the view drives the whole flow. It holds the current phase, a target phase, and a 0.0→1.0 progress value. Every frame reads from it; nothing else animates these elements.
// In the view struct — the single motion owner for this surface.enumPhase { Spawn, Advanced, Fleet, Fullscreen { pane: PaneId }, Diff }
structTransitionState {
from: Phase,
to: Phase,
progress: f32, // 0.0..=1.0, driven by with_animation's delta
}
spawn → advanced (expand). Do not animate the card's width/height. Drive one with_animation whose delta maps onto: rising opacity for the newly-revealed controls, a growing BoxShadow (blur + spread) to read as "lift", and a single layout fraction at the card's container if reflow is unavoidable. Easing: ease_in_out.
advanced → fleet (list reflow). The fleet appears as a list. Stagger entries by feeding each row a phase-offset into the same delta (row i starts at i * 0.04), animating opacity only. No per-row layout animation — the list lays out once; rows fade in over the settled layout.
fleet → fullscreen (zoom). "Zoom" is faked: the chosen pane's container animates a layout fraction from its grid cell toward full bounds while siblings animate opacity down. The apparent scale comes from the fraction + shadow, never scale().
fullscreen → diff (tab-slide). The diff tab slides in by animating a single horizontal layout fraction (offset as a fraction of width, computed once), with opacity rising. One axis, one owner.
Interruption. If the operator clicks away mid-zoom, set to to the new phase and retargetprogress from its current value — never restart at 0. Because one state owns it, the in-flight value is the new animation's start.
Reduced motion. When reduced motion is on, the same state machine resolves progress to 1.0 immediately and skips the loops — but it still applies the final opacity/shadow/fraction so the operator sees which pane expanded and which tab is active. Orientation preserved; travel dropped.
Why it's correct: one owner, interruptible, compositor-friendly (opacity + shadow + a single fraction), reduced-motion keeps orientation, and not one byte of transform. See references/03-transition-architecture.md for the full state-machine scaffold and references/01-gpui-animation-primitives.md for the with_animation signatures.
Quality Gates
Zero scale/translate/rotate/transform usage — all "lift/slide/zoom/spring" composed from opacity, BoxShadow, color, layout-fraction.
No width/height/px/inset animation inside a hot render; any layout animation is a deliberate single fraction at the smallest node.
Exactly one motion owner per surface (one with_animation or one transition state machine); no element written by two animators.
Reduced-motion path resolves to final state and preserves orientation (fade/hint), never a bare snap or deleted feedback.
Every .repeat() loop is scoped to its smallest leaf view and paused/unmounted when idle or off-screen.
Transitions are interruptible: mid-flight changes retarget the current value rather than restarting at 0.
Only confirmed gpui easings used (ease_in_out, bounce, pulsating_between, linear); no ease_out or CSS curve names.
paint/canvas/Vello/wgpu used only for work the element tree genuinely can't express; nothing reimplements div().shadow().
Built and run, not just read: confirmed no runaway re-render on idle, no label overlap, motion ends the instant its job is done.
Fork Guidance
Fork to a subagent when the task is a repo-wide motion audit of a gpui app: inventory every with_animation/repeat(), flag layout-in-hot-render and multi-owner elements, and check reduced-motion coverage across views.
Keep inline when choosing a primitive for one surface, designing a single transition, or reviewing one view's motion.
Good subagent payload: target view/module, surface class (micro | transition | bespoke), the Phases involved, whether reduced motion must be honored, and the frame-budget expectation (idle must be 0 re-renders).
Reference Map
The six grounding docs in references/ — all keyed to real pd-console/src code (gpui 0.2.x, Metal):
references/01-gpui-animation-primitives.md — the four primitives, with_animation / Animation signatures, the delta closure, easing names. Read first.
references/02-the-no-transform-constraint.md — the translation table: how to re-derive lift/slide/zoom/spring without scale/translate/rotate. Read before composing any motion.
references/03-transition-architecture.md — one owner per surface; the interruptible, retargetable transition state machine for spawn → expand → list → zoom → tab-slide.
references/04-frame-budget-and-reduced-motion.md — why .repeat() re-renders forever, scoping loops, and the native reduced-motion discipline (no @media, no compositor thread).
references/05-bespoke-graphics-vello-wgpu.md — the three escape hatches below the widget layer: gpui paint/canvas, PathBuilder, and Vello/wgpu surfaces (ADR-0086).
references/06-motion-aesthetics-and-vocabulary.md — the "splendid, retro-futuristic, buttery" vocabulary: swoosh, glow, hard_offset, breathing dots, control flash, and curves that decelerate like a heavy door settling.
Skill Bundle Index
Every file in this skill, and when to open it. Auto-generated; run scripts/index_references.py --fix.
references/
references/01-gpui-animation-primitives.md — gpui Animation Primitives — > Scope. This is the field manual for motion in pd-console — a native gpui > 0.2.2 app (core/pd-console/Cargo.toml:32: `gpui = {
references/05-bespoke-graphics-vello-wgpu.md — Bespoke Graphics in gpui — Custom Painting, Vello/wgpu Surfaces, and the Cassette-Futurism Layer — > Scope. When the element tree (div().bg().shadow().child()) genuinely cannot express what you need — ordered dithering, a shimmer swe
references/06-motion-aesthetics-and-vocabulary.md — Motion Aesthetics & Vocabulary — > "Splendid, retro-futuristic, buttery." Buttery is not a feeling you bolt on at the end — it is honest 60fps, motion that ends the instan