open-design/craft/animation-discipline.md
marco 5dd70b5016
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m3s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 11s
Initial import: open-design source for helix-mind.ai distribution
This repository contains the open-design daemon CLI source code, built
and packaged at https://helix-mind.ai/cli/open-design/latest.tgz for use
by the HelixMind /design slash command.

Licenses: Apache-2.0 (root) + MIT (skills/*)
2026-05-06 20:50:24 +02:00

155 lines
9.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Animation discipline craft rules
Universal rules for when motion earns its place in a UI and what numbers
constrain it. The active `DESIGN.md` decides brand-specific motion
personality; this file decides whether motion should run at all and at
what duration, easing, and accessibility floor.
> Grounded in primary sources: Tversky/Morrison/Bétrancourt 2002
> (IJHCS), Heer & Robertson TVCG 2007, Harrison/Yeo/Hudson CHI 2010,
> Doherty & Thadani IBM Systems Journal 1982, Chang & Ungar UIST 1993,
> Material 3 motion tokens, IBM `@carbon/motion`, Apple SwiftUI
> Animation API, W3C View Transitions, WCAG 2.2.2 + 2.3.3, WebKit's
> 2017 `prefers-reduced-motion` rationale.
## When motion earns its place
Tversky/Morrison/Bétrancourt's 2002 meta-analysis (IJHCS 57, pp. 247-262)
found that every study claiming animation aids comprehension had a
broken control — the static version had less information, different
procedures, or hidden interactivity. When equalised, animation does
**not** beat static for teaching complex systems. The single use case
the paper endorses is real-time spatial or temporal reorientation:
page transitions, container morphs, viewpoint changes, progress
indicators (p. 257).
A follow-on hazard: Palmiter & Elkerton found animation-trained users
*declined* one week after training, while text-trained users *improved*
(Tversky 2002, p. 255). Animation's apparent short-term parity hides
worse retention.
So animate when the user is moving through space, time, or state —
navigation, container expansion, progress feedback, gesture
follow-through. Don't animate to teach, decorate, signal "premium",
or fill silence.
## Duration thresholds
The cross-design-system convergence is **150 ms** — Material 3 `short3`,
IBM Carbon `moderate-01`, Shopify Polaris `150`, Tailwind default,
SLDS `duration-fast` all land here. Use it as the default duration for
state-confirmation feedback.
| Duration | Use |
|---|---|
| 50100 ms | Instant feedback (button press, toggle commit, hover) |
| 150 ms | Default for state-confirmation |
| 200300 ms | Entering UI (modals, sheets, dropdowns) |
| 300500 ms | Cross-screen transitions, container morphs |
| > 500 ms | Reserved for cross-screen, staged, or platform-native transitions (e.g. M3 `long2`-`extraLong4`, Heer & Robertson 2007's per-stage recommendation). |
Non-navigation microinteractions — hover, press, toggle, validation,
chip selection, row expansion — should stay under 500 ms. Past that the
user notices the motion as motion and waits on the UI rather than
working through it. Two qualifications: frequent animations (a hover
effect seen 50 times per session) need to stay ≤200 ms; mobile
animations should run 2030% shorter than desktop equivalents because
travel distances are shorter.
## Curve vs spring
Use a curve for opacity, color, and any property that changes value
between two known points. Use a spring for position, scale, rotation,
and gesture-driven motion — anything that should feel physical.
Material 3 standard easing is `cubic-bezier(0.2, 0, 0, 1)` — front-loaded;
the trailing zero makes the curve hit its target instantly and settle.
M2 standard was the symmetric `cubic-bezier(0.4, 0, 0.2, 1)`, preserved
in M3 under the name `legacy`. Anyone shipping the M2 curve and calling
it "M3" is on legacy tokens. M3 `emphasized` is a **two-segment Bézier
path**, not a single cubic-bezier; single-cubic approximations silently
lose the front-loaded character. CSS `linear()` (Chrome 113+) is the
only way to replicate it on a single property.
Apple's published SwiftUI default spring is
`(response: 0.5, dampingFraction: 0.825, blendDuration: 0)`. The widely
cited `.snappy = 0.25 s, .smooth = 0.35 s` numbers are wrong — Apple's
docs assign all three presets a 0.5 s base, differing only in bounce
(0 / 0.15 / 0.3).
Spring framework defaults disagree. motion.dev's physics-mode default
is ζ ≈ 0.5 (bouncy). React Spring's `default` is ζ = 0.997 (critically
damped). Same word "default", opposite feel — React Spring's `wobbly`
is the actual feel-equivalent of motion.dev's `default`. Pick
consciously.
## Reduced motion
Every animation that translates, scales, rotates, or parallaxes must
respect `@media (prefers-reduced-motion: reduce)`. WebKit shipped this
in 2017 to address vestibular triggers; the W3C MQ5 spec lets the UA
or author **strip motion entirely or substitute static imagery**
the spec does not mandate which.
Working rule: strip motion-on-an-axis (translate, scale, rotate,
parallax). Keep opacity/color crossfades as substitutes when a state
change still needs to be conveyed. Be explicit — the View Transitions
API does **not** apply `prefers-reduced-motion` automatically; the
author must add a query override on the pseudo-elements or skip
`startViewTransition` entirely.
WCAG calibration: 2.2.2 (Pause/Stop/Hide) is Level A — the legal floor
under ADA Title II 2024 / EN 301 549 / EAA — but it names cognitive,
attentional, and reading populations, not vestibular. Vestibular
language lives in 2.3.3, which is **AAA**. Don't conflate the two.
Building for vestibular users is a craft commitment beyond the legal
floor, not a WCAG mandate.
**Flashing limits.** WCAG 2.3.1 (Level A) permits flashing only when
there are no more than three flashes within any one-second period, or
the flashing area stays below the general and red flash thresholds.
WCAG 2.3.2 (AAA) forbids flashing more than three times within any
one-second period, regardless of area or brightness. The protected
concern is photosensitive epilepsy; the legal floor isn't negotiable. For gamified UI, onboarding celebrations, sparkles,
confetti, level-up bursts, and shimmer: avoid rapid flashing unless
tested against the thresholds, and prefer one-shot animations over
loops.
## Repeated and ambient motion
The rules above target one-shot transitions. Looping motion (skeleton
shimmer, idle backgrounds, autoplay, reward bursts) has different
constraints.
- Cap iteration count: carousels at 3-5 cycles then pause; skeleton shimmer until content lands, never indefinitely.
- WCAG 2.2.2 (Level A) requires a pause control for any motion running longer than 5 seconds — moving, blinking, or scrolling content, not only video.
- Cancel ambient motion on route change.
- Reward animations are one-shot. Confetti, sparkles, level-up bursts fire once and dismiss; no looping timer.
- Spinners must not run indefinitely. Escalate to progress/cancel states and stop animation at 60 s, matching `state-coverage.md`.
## Cross-platform handoff
Native conventions diverge.
- **iOS** uses spring physics with perceptual `(response, dampingFraction)` parameters. Apple HIG documents principles, not numerical curves; the SwiftUI Animation API JSON is the source for actual numbers. UIView curve cubic-beziers commonly cited online are reverse-engineered, not Apple-published.
- **Android** uses cubic-bezier curves through M3 motion tokens (501000 ms range, 16 named durations). Predictive back is a *gesture-progress primitive*, not a transition primitive — `BackEvent.progress` is sampled per-frame from the touch stream and the destination is rendered behind the current surface while still on it. Cancellation is a first-class lifecycle state.
- **Web** has the View Transitions API (default 0.25 s, no easing specified by the spec — falls through to CSS `ease`). Same-document support 90.94%; cross-document 87.82%. Cross-document is same-origin and user-initiated only.
A "one curve fits all platforms" approach loses on each. If the brief
specifies platform fidelity, follow the platform; if it specifies brand
consistency, pick one motion vocabulary and apply it everywhere.
## Common mistakes (lint these)
- "Skeleton screens feel 11% faster" — Harrison/Yeo/Hudson CHI 2010 measured *backwards-decelerating ribbed determinate progress bars* (n=16). The induced-motion mechanism doesn't transfer to skeletons.
- "Heer & Robertson recommend 3001000 ms eased transitions" — they tested 1.25 s and 2 s only. Their recommendation is "~1 second per stage".
- "Doherty Threshold = 400 ms" — the 1982 paper does not contain "400". The lowest threshold actually measured is 300 ms.
- M2 standard easing `cubic-bezier(0.4, 0, 0.2, 1)` labelled as "Material 3". M3's standard is `cubic-bezier(0.2, 0, 0, 1)`.
- Animations that *perform* a state change rather than *confirming* one that has already happened. Optimistic UI first; motion second.
- More than 500 ms on any non-cross-screen transition.
- Animation as the only signal of state change. Reduced-motion users miss it; always pair with a static affordance (color, position, label).
- Ignoring `prefers-reduced-motion` on transform-based animations — the highest-cost vestibular triggers.
- Curve-based animation on a `transform: scale()` that should feel physical. Use a spring.
- Hero choreography in productivity tools. Motion budget belongs inside the product on functional micro-feedback, not on landing-page sequences.
- Decorative motion in the working canvas of a productivity tool.