Atelier — Motion & Micro-interactions
Motion is what separates a premium interface from a static one — and bad motion (too slow, too much,
wrong easing, animating the wrong property) instantly reads cheap. The rule that governs everything
here: motion should be felt, not seen. If the user consciously waits on an animation, it's too slow
or too big.
Project memory: if ATELIER.md exists, read its Interactivity level and Motion policy
(library triggers, easing, reduced-motion) first and honor them — they set the budget. Set up memory with
/atelier init (the atelier router).
Inputs: the Direction Doc's motion budget (none / restrained / award-grade). Default stack:
Motion (motion/react) for component/gesture/layout/exit animation; GSAP for complex sequencing;
scroll-driven motion → atelier-scroll; the perf/a11y gate → atelier-perf-a11y (animate
only transform/opacity, respect reduced motion); where these hooks mount in a real component tree
→ atelier-components. The words in the states you build (empty/error/loading) → atelier-copy;
making the optimistic/skeleton/error states production-grade (rollback, no stuck spinner) → atelier-harden.
Deep reference: references/fundamentals-deepdive.md
(§1, §5).
The flow
- Set the budget (from the Direction Doc) → 2. Pick durations + easing → 3. Choose the tool
→ 4. Build the interaction → 5. Add the missing states → 6. Gate it (reduced motion + perf).
1. Motion budget
Match motion to the world (Direction Doc):
- Production — purposeful only: feedback (<100ms), state changes, attention guidance, latency masking.
Durations ≤300ms, one thing moving at a time. Restraint reads as confidence.
- Award/creative — motion is part of the brand; bigger gestures, choreography, signature moments.
Still ships reduced-motion fallbacks.
When in doubt, do less. The most common slop tell is too much motion (infinite-loop micro-animations
everywhere), not too little.
2. Durations & easing (defaults to internalize)
Full table + cubic-beziers + spring params in references/timing-easing.md. The essentials:
- Hover / press / toggle: 120–200ms. Reveal / small move: 200–300ms. Modal / drawer / page:
300–450ms. Ceiling ~500ms. Respond to input ≤100ms always (immediate press/scale).
- Easing by intent: ease-out for entrances (fast→settle, feels responsive); ease-in for
exits; ease-in-out for moves between two on-screen states; linear only for continuous (spinner,
marquee, progress).
- Springs for direct manipulation / interruptible UI (drag, sheets, toggles) — they carry velocity
and feel physical. Duration+easing for deterministic, choreographed sequences.
- Distance/size scale duration (bigger/farther → slightly longer).
3. Choose the tool
- Motion (
motion/react) — the default for React UI: declarative animate/whileHover/whileTap/
whileInView, AnimatePresence for exit animations, layout/layoutId for shared-element "magic
move", springs, gestures. Springs are the default only for transforms (x/y/scale/rotate); opacity/
color default to tween/ease. Recipes in references/motion-react.md.
- react-spring (
@react-spring/web) — lighter pure-springs alternative when springs are the whole
story and you want R3F interop; reach for Motion when you need orchestration/exit/shared-element.
- GSAP — reach for it for complex timelines, SVG morph/draw, or when you need a single engine for an
intricate sequence. Also the home of the Flip plugin for FLIP layout / shared-element transitions
between two arbitrary DOM states (expand/collapse, grid reflow, reorder) — the explicit-control
escalation for when Motion's
layout/layoutId (the React default) or Auto-Animate's auto-diff don't
fit; recipe in references/microinteractions-states.md (GSAP plugins are all free now — details there).
(Scroll-driven motion and pointer-driven flourishes — magnetic buttons, custom cursor, infinite
marquee — live in atelier-scroll, not here.)
- Anime.js v4 (
animejs ~4.4, ESM named imports — no global anime; v3 code won't run) — free,
tree-shakeable general timeline for vanilla / non-Motion contexts; built-in onScroll() observer and
svg morph/draw, plus a ~3KB waapi adapter for WAAPI-only cases. Reach for it over GSAP when you need a
light timeline, not the full engine.
- Native CSS — transitions and keyframes are perfect for simple hover/toggle/state changes with zero
JS; prefer them when they suffice (they're cheap and run off the main thread). For entry and exit
with no JS,
@starting-style + transition-behavior: allow-discrete animates popovers/dialogs/toasts
in and out (Baseline) — often replacing <AnimatePresence> for lightweight reveals.
- Auto-Animate (
@formkit/auto-animate ~0.9) — one-line FLIP for list add/remove/move when you want
zero config; ~2KB, respects reduced motion out of the box.
- Designer-authored / interactive assets — play After-Effects motion with Lottie, preferring
@lottiefiles/dotlottie-web (Rust+WASM on ThorVG 1.0; DotLottieWorker for off-main-thread GPU
playback) over lottie-web. For interactive, stateful, data-bound graphics (UI characters, game-like
UI) use Rive — state machines + data binding live in the .riv (@rive-app/react-canvas;
@rive-app/webgl2 Rive Renderer is the default, @rive-app/canvas for grids/lists). Both are playback
of pre-authored assets — build component interactions on Motion, not here.
Don't start new work on Motion One (folded into Motion — use motion/motion/mini), Popmotion, or
Velocity (all frozen); treat Theatre.js as dormant (stale 0.7.2 public release, 1.0 private).
4. Build the interaction
Use the patterns in references/motion-react.md. Core moves: variants with staggerChildren for lists,
AnimatePresence for mount/unmount, layout for size/position changes, useReducedMotion() to branch.
Keep to transform/opacity. Stagger 30–80ms/item, total cascade <600ms.
- Reveal-safety (don't ship a blank page): scroll/entrance reveals must enhance an already-visible
default — never gate content visibility on a class- or JS-triggered transition. Transitions pause on
hidden tabs and don't fire in SSR / headless / no-JS renders, so the content stays at opacity 0 and the
section ships blank. Animate from a visible baseline (content present in the DOM and legible, then
enhanced), and make the reduced-motion / no-JS path show the final state immediately.
- Suppress the uniform-entrance reflex: one identical fade-up applied to every section is itself a tell.
Each reveal should fit what it reveals; staggering items within one list is fine — cloning one entrance
across the whole page is not. (Verified at ship by
atelier-perf-a11y's anti-slop check.)
5. Add the missing states (the part everyone skips)
A premium feel is mostly about the non-happy states (which ones each screen needs is enumerated upstream
by atelier-ux; here you build them). Patterns in
references/microinteractions-states.md:
- Skeleton / shimmer loaders (perceived speed > spinner; static under reduced motion).
- Optimistic UI —
useOptimistic (React 19): update instantly, reconcile/rollback. Only for
likely-success, cheap-to-reverse actions (likes, toggles, list adds) — never irreversible/financial.
- Empty / loading / error states designed deliberately (empty = onboarding + CTA; error = human
language + cause + recovery, preserve input).
- Micro-interactions — button press/scale, toggle, like, copy-confirm, input focus; tie a clear
signifier to every action (Don Norman: affordance → signifier → feedback ≤100ms).
- Animated numbers —
NumberFlow (@number-flow/react) for counters/prices/stats: accessible,
Intl.NumberFormat-aware, respects reduced motion by default. The correct default over a hand-rolled
(janky + inaccessible) odometer.
- Haptics —
navigator.vibrate for short confirmations, but Android Chrome/Edge only (~77%); NOT
iOS Safari, NOT Firefox. Mobile-only, needs a user gesture, never haptic-only — always pair a visible cue.
6. Gate it
- Reduced motion is mandatory.
useReducedMotion() (Motion) or @media (prefers-reduced-motion: reduce). Reduce, don't always nuke — keep opacity fades and shortened durations; remove large
movement, parallax, spin, springy overshoot. Provide non-motion signifiers so no info is lost.
- Perf: animate only
transform/opacity; prefer CSS/WAAPI/Motion (off main thread) over JS rAF
tweening of layout/paint props. A transform-only rAF loop (e.g. a lerped custom cursor in
atelier-scroll) is fine — the rule targets width/top/box-shadow, not transform. Run the full
atelier-perf-a11y gate before shipping.
The 12 principles → UI (mental backbone)
Squash&stretch (press squish), anticipation (wind-up), staging (dim bg for modal), follow-through/
overlap (stagger, spring settle), slow in/out = easing (the most important), arcs (curved paths),
secondary action, timing (conveys weight), exaggeration (sparingly, for delight), appeal (brand
personality). Full mapping in references/timing-easing.md.
Operating principles
- Felt, not seen. Subtle + fast by default; reserve big gestures for one signature moment.
- One thing moving at a time in production UI; orchestrate with overlap, not chaos.
- Springs for manipulation, easing for choreography.
- Design the empty/loading/error states — that's where perceived quality actually lives.
- Always reduced-motion +
transform/opacity only. Beauty ≠ inaccessible or janky.
1---2name: atelier-motion3description: Atelier suite — motion & micro-interactions (the "feel" layer). Add animation that reads premium, not gimmicky: correct easing/spring defaults, the right durations, the 12 animation principles applied to UI, component/gesture/layout/exit animation with Motion (motion/react), micro-interactions (buttons, toggles, likes), and the often-skipped states (skeleton/shimmer, optimistic UI, empty/loading/error). Use whenever adding or fixing UI animation, transitions, hover/press feedback, loading states, or when motion feels janky, abrupt, excessive, or cheap. Default stack: Motion for React UI — with Anime.js v4 / native CSS / GSAP for vanilla & non-React work, and Lottie / Rive for designer-authored assets; defer scroll-driven work to atelier-scroll and pre-ship checks to atelier-perf-a11y. Part of the Atelier suite.4---56# Atelier — Motion & Micro-interactions78Motion is what separates a premium interface from a static one — and bad motion (too slow, too much,9wrong easing, animating the wrong property) instantly reads cheap. The rule that governs everything10here: **motion should be felt, not seen.** If the user consciously waits on an animation, it's too slow11or too big.1213> **Project memory:** if **`ATELIER.md`** exists, read its **Interactivity** level and **Motion policy**14> (library triggers, easing, reduced-motion) first and honor them — they set the budget. Set up memory with15> **`/atelier init`** (the **`atelier`** router).16>17> **Inputs:** the Direction Doc's *motion budget* (none / restrained / award-grade). **Default stack:**18> Motion (`motion/react`) for component/gesture/layout/exit animation; GSAP for complex sequencing;19> **scroll-driven motion → `atelier-scroll`**; **the perf/a11y gate → `atelier-perf-a11y`** (animate20> only `transform`/`opacity`, respect reduced motion); **where these hooks mount in a real component tree21> → `atelier-components`.** The *words* in the states you build (empty/error/loading) → **`atelier-copy`**;22> making the optimistic/skeleton/error states production-grade (rollback, no stuck spinner) → **`atelier-harden`**.23> Deep reference: `references/fundamentals-deepdive.md`24> (§1, §5).2526---2728## The flow29301. **Set the budget** (from the Direction Doc) → 2. **Pick durations + easing** → 3. **Choose the tool**31→ 4. **Build the interaction** → 5. **Add the missing states** → 6. **Gate it** (reduced motion + perf).3233## 1. Motion budget3435Match motion to the world (Direction Doc):36- **Production** — purposeful only: feedback (<100ms), state changes, attention guidance, latency masking.37 Durations ≤300ms, one thing moving at a time. Restraint reads as confidence.38- **Award/creative** — motion is part of the brand; bigger gestures, choreography, signature moments.39 Still ships reduced-motion fallbacks.40When in doubt, do less. The most common slop tell is *too much* motion (infinite-loop micro-animations41everywhere), not too little.4243## 2. Durations & easing (defaults to internalize)4445Full table + cubic-beziers + spring params in **`references/timing-easing.md`**. The essentials:46- **Hover / press / toggle: 120–200ms.** Reveal / small move: 200–300ms. Modal / drawer / page:47 300–450ms. **Ceiling ~500ms.** Respond to input **≤100ms** always (immediate press/scale).48- **Easing by intent:** ease-**out** for entrances (fast→settle, feels responsive); ease-**in** for49 exits; ease-in-out for moves between two on-screen states; **linear only** for continuous (spinner,50 marquee, progress).51- **Springs for direct manipulation / interruptible UI** (drag, sheets, toggles) — they carry velocity52 and feel physical. Duration+easing for deterministic, choreographed sequences.53- Distance/size scale duration (bigger/farther → slightly longer).5455## 3. Choose the tool5657- **Motion (`motion/react`)** — the default for React UI: declarative `animate`/`whileHover`/`whileTap`/58 `whileInView`, `AnimatePresence` for exit animations, `layout`/`layoutId` for shared-element "magic59 move", springs, gestures. Springs are the default **only for transforms** (x/y/scale/rotate); opacity/60 color default to tween/ease. Recipes in **`references/motion-react.md`**.61- **react-spring** (`@react-spring/web`) — lighter pure-springs alternative when springs are the whole62 story and you want R3F interop; reach for Motion when you need orchestration/exit/shared-element.63- **GSAP** — reach for it for complex timelines, SVG morph/draw, or when you need a single engine for an64 intricate sequence. Also the home of the **Flip** plugin for FLIP layout / shared-element transitions65 between two *arbitrary* DOM states (expand/collapse, grid reflow, reorder) — the explicit-control66 escalation for when Motion's `layout`/`layoutId` (the React default) or Auto-Animate's auto-diff don't67 fit; recipe in `references/microinteractions-states.md` (GSAP plugins are all free now — details there).68 (Scroll-driven motion **and** pointer-driven flourishes — magnetic buttons, custom cursor, infinite69 marquee — live in `atelier-scroll`, not here.)70- **Anime.js v4** (`animejs` ~4.4, ESM **named** imports — no global `anime`; v3 code won't run) — free,71 tree-shakeable general timeline for **vanilla / non-Motion** contexts; built-in `onScroll()` observer and72 `svg` morph/draw, plus a ~3KB `waapi` adapter for WAAPI-only cases. Reach for it over GSAP when you need a73 light timeline, not the full engine.74- **Native CSS** — transitions and keyframes are perfect for simple hover/toggle/state changes with zero75 JS; prefer them when they suffice (they're cheap and run off the main thread). For entry **and** exit76 with no JS, `@starting-style` + `transition-behavior: allow-discrete` animates popovers/dialogs/toasts77 in and out (Baseline) — often replacing `<AnimatePresence>` for lightweight reveals.78- **Auto-Animate** (`@formkit/auto-animate` ~0.9) — one-line FLIP for list add/remove/move when you want79 zero config; ~2KB, respects reduced motion out of the box.80- **Designer-authored / interactive assets** — play After-Effects motion with **Lottie**, preferring81 **`@lottiefiles/dotlottie-web`** (Rust+WASM on ThorVG 1.0; `DotLottieWorker` for off-main-thread GPU82 playback) over `lottie-web`. For *interactive, stateful, data-bound* graphics (UI characters, game-like83 UI) use **Rive** — state machines + data binding live in the `.riv` (`@rive-app/react-canvas`;84 `@rive-app/webgl2` Rive Renderer is the default, `@rive-app/canvas` for grids/lists). Both are playback85 of pre-authored assets — build component interactions on Motion, not here.8687> **Don't start new work on** Motion One (folded into Motion — use `motion`/`motion/mini`), Popmotion, or88> Velocity (all frozen); treat Theatre.js as dormant (stale 0.7.2 public release, 1.0 private).8990## 4. Build the interaction9192Use the patterns in `references/motion-react.md`. Core moves: variants with `staggerChildren` for lists,93`AnimatePresence` for mount/unmount, `layout` for size/position changes, `useReducedMotion()` to branch.94Keep to `transform`/`opacity`. Stagger 30–80ms/item, total cascade <600ms.9596- **Reveal-safety (don't ship a blank page):** scroll/entrance reveals must *enhance an already-visible97 default* — never gate content visibility on a class- or JS-triggered transition. Transitions pause on98 hidden tabs and don't fire in SSR / headless / no-JS renders, so the content stays at opacity 0 and the99 section ships blank. Animate *from* a visible baseline (content present in the DOM and legible, then100 enhanced), and make the reduced-motion / no-JS path show the final state immediately.101- **Suppress the uniform-entrance reflex:** one identical fade-up applied to every section is itself a tell.102 Each reveal should fit what it reveals; staggering items *within* one list is fine — cloning one entrance103 across the whole page is not. (Verified at ship by `atelier-perf-a11y`'s anti-slop check.)104105## 5. Add the missing states (the part everyone skips)106107A premium feel is mostly about the *non-happy* states (*which* ones each screen needs is enumerated upstream108by **`atelier-ux`**; here you build them). Patterns in109**`references/microinteractions-states.md`**:110- **Skeleton / shimmer** loaders (perceived speed > spinner; static under reduced motion).111- **Optimistic UI** — `useOptimistic` (React 19): update instantly, reconcile/rollback. Only for112 likely-success, cheap-to-reverse actions (likes, toggles, list adds) — never irreversible/financial.113- **Empty / loading / error** states designed deliberately (empty = onboarding + CTA; error = human114 language + cause + recovery, preserve input).115- **Micro-interactions** — button press/scale, toggle, like, copy-confirm, input focus; tie a clear116 signifier to every action (Don Norman: affordance → signifier → feedback ≤100ms).117- **Animated numbers** — `NumberFlow` (`@number-flow/react`) for counters/prices/stats: accessible,118 `Intl.NumberFormat`-aware, respects reduced motion by default. The correct default over a hand-rolled119 (janky + inaccessible) odometer.120- **Haptics** — `navigator.vibrate` for short confirmations, but **Android Chrome/Edge only (~77%); NOT121 iOS Safari, NOT Firefox.** Mobile-only, needs a user gesture, never haptic-only — always pair a visible cue.122123## 6. Gate it124125- **Reduced motion is mandatory.** `useReducedMotion()` (Motion) or `@media (prefers-reduced-motion:126 reduce)`. **Reduce, don't always nuke** — keep opacity fades and shortened durations; remove large127 movement, parallax, spin, springy overshoot. Provide non-motion signifiers so no info is lost.128- **Perf:** animate only `transform`/`opacity`; prefer CSS/WAAPI/Motion (off main thread) over JS rAF129 tweening **of layout/paint props**. A transform-only rAF loop (e.g. a lerped custom cursor in130 `atelier-scroll`) is fine — the rule targets `width/top/box-shadow`, not `transform`. Run the full131 **`atelier-perf-a11y`** gate before shipping.132133---134135## The 12 principles → UI (mental backbone)136Squash&stretch (press squish), anticipation (wind-up), staging (dim bg for modal), follow-through/137overlap (stagger, spring settle), **slow in/out = easing (the most important)**, arcs (curved paths),138secondary action, timing (conveys weight), exaggeration (sparingly, for delight), appeal (brand139personality). Full mapping in `references/timing-easing.md`.140141## Operating principles142- **Felt, not seen.** Subtle + fast by default; reserve big gestures for one signature moment.143- **One thing moving at a time** in production UI; orchestrate with overlap, not chaos.144- **Springs for manipulation, easing for choreography.**145- **Design the empty/loading/error states** — that's where perceived quality actually lives.146- **Always reduced-motion + `transform`/`opacity` only.** Beauty ≠ inaccessible or janky.