SuperAPI AI Directory

motion

General & other

View repository

Updated 10-15 19:52

About this skill

Moving One Element

The default is no animation. An element earns motion only by answering a question the user is already asking — where did this come from, did my press register, what just changed — and if it answers none of them, ship it static. When motion is earned, the default shape is a CSS transition on transform and opacity, ease-out, under 300ms, with a prefers-reduced-motion path written in the same commit rather than a follow-up. This skill governs one element on its own clock: the moment a second element has to coordinate with the first — stagger, a shared element, a route change — that is transitions, and the moment the clock is a finger rather than a timer, that is gestures.

Read the project's motion system before writing a line. Grep for --ease, cubic-bezier, @theme, transition-timing-function, motion/framer-motion in package.json, useSpring, animate(, or a tokens/theme file. Whatever declares timing already wins: write your change in that mechanism, reuse its named curves and durations, and never introduce a second animation layer to fix one element. If the project ships a curve you disagree with, say so as a finding — do not quietly ship a competing one.

Quick Reference

When Open
Before writing a modal, drawer, popover, dropdown, tooltip, toast, or accordion from scratch — each has an exact recipe you should not re-derive patterns.md
When the default curve set is not enough, a crossfade looks wrong, you need JS control over a CSS-quality animation, or you must pick the project's house curve family techniques.md

Framework 1 — should this animate at all?

Gate on frequency first. This is not a judgment call.

The user triggers it Decision
100+ times a day (command palette, shortcuts) No animation. Ever.
Tens of times a day (hover, list navigation) Remove or drastically reduce
Occasionally (modal, drawer, toast) Standard animation
Rarely / first run (onboarding, celebration) Delight is affordable here

Emil Kowalski's gate; Raycast ships no open/close animation on its command menu for exactly this reason. Never animate a keyboard-initiated action — arrow-key list navigation, shortcuts, focus jumps. Keyboard users move faster than any curve, and motion there reads as lag.

Framework 2 — which curve, which duration?

Entering or exiting → ease-out. Already on screen, moving or resizing → ease-in-out. Hover, color, shadow → ease. Constant-rate (spinner, progress-as-time) → linear.

Built-in keywords are too weak to read as intentional. Ship three tokens (Emil Kowalski; the drawer curve is Ionic Framework's):

--ease-out:    cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
Element Duration
Button press feedback 100–160ms
Tooltips, small popovers 125–200ms
Dropdowns, selects 150–250ms
Modals, drawers 200–500ms
Marketing / explanatory May run longer

Core Principles

  1. Never enter from scale(0). Nothing in the real world appears from nothing, so the eye reads it as a glitch rather than an arrival. Start at scale(0.95) with opacity: 0 — 0.97 for anything under roughly a tooltip's size. Exception: a contextual icon cross-swap goes deeper, scale(0.3) → 1, because on a 16px glyph a timid scale reads as a flicker instead of a swap.

  2. Never use ease-in on a UI transition. It back-loads the movement across the exact frames the user is watching most closely; ease-out at 200ms feels faster than ease-in at the same 200ms. Exception: the exit half of an enter/exit pair, where a slow start is the quietness you want.

  3. Stay under 300ms in product UI. "A 180ms dropdown feels more responsive than a 400ms one." The 200–500ms modal/drawer band above is not a licence: the 300–500ms half of it belongs to full-viewport travel — a mobile sheet crossing the whole screen — and everything else lives under 300ms. Exception: marketing and explanatory motion, which obeys marketing-pages, not this ceiling.

  4. Scale popovers, dropdowns, and tooltips from their trigger. A menu that grows from the centre of the screen severs the link between the control pressed and the surface that appeared. Use transform-origin: var(--transform-origin) (Base UI; Radix exposes --radix-popper-transform-origin). Exception: modals, which are anchored to the viewport rather than a trigger — transform-origin: center is correct there, and reporting it is a worse error than missing it.

  5. Press feedback is scale(0.97) on :active, as a transition at 160ms ease-out. A transition lets a release mid-press glide back instead of snapping; keep the value in 0.95–0.98, since below 0.95 it turns cartoonish. Exception: dense or high-frequency controls skip it — bake an opt-out prop into the Button rather than removing the transition.

  6. Reversible state gets a transition; one-shots get @keyframes. A transition retargets from wherever the element currently is, so closing a drawer mid-open reverses from that position; a keyframe restarts at frame one and looks broken. Exception: sequences that always run to completion — a loader, a first-run entrance — where a keyframe is the simpler tool.

  7. Exits run 20–30% faster than enters, and travel less. The user has already decided to leave. Enter 250ms / exit 180ms, nudging translateY(-8px) rather than the element's own height, and never cutting to display: none, which severs spatial context. Exception: when the destination is the information — a swiped-away card, a closing drawer — the full off-screen travel is the point.

  8. Animate transform and opacity only. Everything else forces layout or paint on the whole subtree; transition: all is a finding on sight. Exception: clip-path and filter: blur() are legitimate and hardware-friendly, with blur capped under 20px because it is expensive, especially in Safari — see techniques.md.

  9. Do not animate resting state on first paint. A component already sitting where it belongs when the page appears has no entrance to play; in Motion, set initial={false} on the AnimatePresence wrapper for toggles, tabs, and icon swaps. Exception: a genuine first-run entrance depends on its initial prop — initial={false} there deletes the whole thing.

  10. Reduced motion means gentler, not zero. Keep opacity and colour response so the interface still answers the user; drop transform-based movement. Exception: large-surface vestibular motion — full-viewport parallax, a moving background — is removed outright rather than softened.

Smell / Fix

Smell Fix
transition: all 400ms ease-in Name the properties, 200ms, var(--ease-out)
Enter from scale(0) or translateY(100vh) scale(0.95) + opacity: 0, short travel
Animating height, width, margin, top transform + opacity; grid-template-rows or clip-path for expansion
Popover scales from centre transform-origin: var(--transform-origin)
Modal and overlay on different durations Paired elements share duration and curve exactly
@keyframes on an open/close toggle A transition, so it can be interrupted
Motion on arrow-key navigation Remove it — keyboard-initiated actions never animate
Exit longer or louder than the enter Exit 20–30% faster, translateY(-8px)
Hover state flickers Animate a child so the parent's hover area stays still
1px jitter at the start or end will-change: transform on that element only
No reduced-motion path Add it in the same commit, not the next one
A new dependency for a fade Use the project's existing stack

Output

Ship the value, never the adjective. Every proposal names the property, the duration in milliseconds, and the curve as a token — transform 200ms var(--ease-out) — plus the reduced-motion path. "Tighten the timing" is not an answer.

Checklist

  • Frequency gate passed, and the animation's purpose is nameable in one word
  • transform / opacity only; no transition: all
  • Curve matches enter / move / hover / linear, and is a project token
  • Duration under 300ms (or justified full-viewport travel), exit faster than enter
  • Enters from scale(0.95)–0.97, never scale(0)
  • Origin-aware if it is anchored to a trigger; centred if it is a modal
  • Reversible state uses a transition, not @keyframes
  • :active feedback present unless the control is high-frequency
  • prefers-reduced-motion path exists and keeps opacity response
  • Written in the project's existing motion system, not beside it