Motion & Animation Logic (Desktop)
General motion craft here draws on well-established, publicly documented UI-animation principles (the kind covered in community references like the MIT-licensed emilkowalski/skills collection) — restated and reprioritized here for a desktop app's specific needs: a keyboard-heavy power user, and hardware that can be as modest as 8GB RAM / 2GB VRAM, where GPU-cheap animation isn't optional polish, it's a functional requirement.
Question 1: should this even animate?
Desktop apps get used for hours, and some actions repeat hundreds of times a session. Motion that's charming the first time becomes friction by the fiftieth.
| How often the user triggers it | Verdict |
|---|---|
| Keyboard shortcuts, command-palette actions, anything fired dozens+ times a session | No animation, ever — instant state change |
| Frequent UI (hover states, list item selection) | Minimal or none — a color/opacity shift at most |
| Occasional (opening a modal, a panel, a settings screen) | Standard, brief animation is appropriate |
| Rare / first-run (onboarding, a completed-task celebration, first successful export) | Room for a bit more personality and duration |
Never animate an action the user triggered by keyboard. It's the fastest way to make a power-user tool feel like it's fighting the user's own speed.
Question 2: what's it for?
Every animation should answer one of these — if it can't, cut it:
- Feedback — confirms the interface registered the action (button press feedback).
- Continuity — helps the user track where something went or came from (a panel sliding in from the edge it's anchored to, a popover scaling from its trigger).
- State change communication — makes an otherwise-invisible state transition visible (a toggle sliding, a tab indicator moving).
- Preventing a jarring pop — anything appearing/disappearing with zero transition reads as broken, not efficient; a very short fade is often enough here even when nothing fancier is warranted.
"It looks cool" is not on this list. If that's the only justification and the element is seen often, don't animate it.
Easing and duration
Use consistent, purposeful easing rather than relying on default browser/engine curves, which tend to feel weak and un-intentional:
| Situation | Easing | Typical duration |
|---|---|---|
| Element entering (modal open, panel slide-in, popover appear) | Ease-out (fast start, settles gently) | 150–250ms |
| Element exiting | Ease-out, and noticeably faster than its own entrance | 100–180ms |
| Moving/morphing on screen (drag settle, resize) | Ease-in-out | 200–400ms |
| Hover/color shifts | Standard ease | 100–150ms |
| Constant motion (progress indicator, marquee) | Linear | n/a |
Never use ease-in alone for something entering the screen — a slow start on an entrance makes the interface feel like it's lagging behind the user's action, which is the opposite of what a button press or panel open should communicate.
Keep functional UI animation under ~300ms. Beyond that it starts to read as slow rather than smooth, especially for anything that repeats. Save longer, more elaborate motion for genuinely rare, celebratory moments.
Exit faster than enter. A dialog can take 200ms to arrive and 120ms to leave — arriving is something the user is watching for and benefits from a settle; leaving should just get out of the way.
The core feedback pattern
The highest-leverage single animation in any app: pressable elements should visibly compress on press.
.control {
transition: transform 150ms ease-out;
}
.control:active {
transform: scale(0.97);
}
Apply this to buttons, list items, cards — anything clickable. It costs almost nothing to implement and does more to make an interface "feel alive" than any other single change.
Entrances: never from nothing
An element appearing from scale(0) looks like it teleported in from nowhere — nothing in the physical world does that. Start from a near-full scale plus opacity instead:
/* Avoid */
.entering { transform: scale(0); }
/* Prefer */
.entering { transform: scale(0.95); opacity: 0; }
Origin-aware scaling
Popovers, dropdowns, and context menus should scale from the point they're anchored to (the trigger), not from the center of the screen. Modals are the deliberate exception — since they're not anchored to any specific control, they should scale/fade from the viewport center. See desktop-ui-surfaces-overlays.
Performance rules (non-negotiable on this hardware target)
- Animate only
transformandopacity. These skip layout and paint and run cheaply on the GPU. Animatingwidth,height,padding, ormarginforces the browser/engine to recompute layout on every frame — expensive, and especially bad on 2GB-VRAM hardware. - Prefer CSS transitions over keyframe animations for anything that can be re-triggered rapidly (a toast queue, a rapidly toggled panel) — transitions can be interrupted and smoothly retarget mid-motion; keyframe animations restart from zero, causing a visible stutter if triggered again before finishing.
- Avoid real-time blur as a routine effect. A one-off, small
backdrop-filter: blur()on a static overlay is fine; blurring something that's actively animating or resizing every frame is one of the more expensive things you can ask this hardware target to do — use a solid or lightly-tinted backdrop instead (seedesktop-ui-surfaces-overlays). - Respect
prefers-reduced-motion. Reduced motion means calmer, not zero — keep opacity/color transitions that help the user track state changes, but drop transform-based movement.
Stagger, sparingly
When several items enter together (a list populating, a grid of results), a small stagger (30–60ms between items) reads as more natural than everything popping in simultaneously — but keep the total stagger window short (a 20-item list shouldn't take 2 seconds to finish animating in), and never block interaction while it plays.
Review format
| Before | After | Why |
|---|---|---|
transition: all 300ms on a panel |
transition: transform 200ms ease-out, opacity 200ms ease-out |
Explicit properties avoid accidentally transitioning things you didn't intend, and are easier to tune |
| Modal closes in the same 250ms it took to open | Opens in 220ms, closes in 130ms | Exit should get out of the way faster than entrance draws attention |
A settings toggle animated by animating width |
Same toggle animated via transform: translateX() |
Transform-based animation is GPU-cheap; width/layout animation is not |
| Keyboard-triggered panel toggle has a 250ms slide animation | Instant, no animation | Frequently keyboard-triggered actions should never be slowed down by motion |