$npx -y skills add heymegabyte/claude-skills --skill 11-motion-and-interaction-systemMeaning-first animation with 3-tier hierarchy. CSS scroll-driven (animation-timeline: scroll()), View Transitions API, @starting-style DOM-insert, container scroll-state queries, prefers-reduced-motion mandatory on all animations.
| 1 | # 11 — Motion and Interaction System |
| 2 | |
| 3 | Apply meaning-first CSS animations (scroll-driven, View Transitions, `@starting-style`) with mandatory `prefers-reduced-motion` on every animated surface. |
| 4 | |
| 5 | ## Motion serves one of three purposes |
| 6 | |
| 7 | 1. **Feedback** — confirm user action (button press, form submit, save) |
| 8 | 2. **Continuity** — preserve spatial context across state changes (page transition, modal open) |
| 9 | 3. **Delight** — express brand personality (hero parallax, signature reveal) |
| 10 | |
| 11 | Anything else = AI slop. Cut it. |
| 12 | |
| 13 | ## 3-Tier Hierarchy |
| 14 | |
| 15 | - **Tier 1 — Functional** — feedback on every interaction (hover, focus, active, tap). Duration 100–200ms. Transform/opacity only. |
| 16 | - **Tier 2 — Choreographic** — page transitions, modal entrance, section reveal. Duration 300–500ms. View Transitions or `@starting-style`. |
| 17 | - **Tier 3 — Cinematic** — hero parallax, signature reveal, scroll-driven storytelling. Duration ≥600ms. Scroll-timeline. |
| 18 | |
| 19 | Never stack 3 tiers on same surface. One cinematic per page. |
| 20 | |
| 21 | ## Mandatory `prefers-reduced-motion` |
| 22 | |
| 23 | EVERY animation MUST honor `prefers-reduced-motion: reduce` — snap to final state, never hide content. |
| 24 | |
| 25 | ```css |
| 26 | @media (prefers-reduced-motion: reduce) { |
| 27 | *, *::before, *::after { |
| 28 | animation-duration: 0.01ms !important; |
| 29 | animation-iteration-count: 1 !important; |
| 30 | transition-duration: 0.01ms !important; |
| 31 | scroll-behavior: auto !important; |
| 32 | } |
| 33 | } |
| 34 | ``` |
| 35 | |
| 36 | ## View Transitions API |
| 37 | |
| 38 | - Same-document SPA: `document.startViewTransition(() => updateDOM())` |
| 39 | - Cross-document MPA: `@view-transition { navigation: auto; }` in CSS |
| 40 | - Per-element: `view-transition-name: hero-image;` on persistent elements |
| 41 | - Chrome 126+, Safari 18+, Firefox 144 (Oct 2025) |
| 42 | |
| 43 | ```css |
| 44 | ::view-transition-old(root), ::view-transition-new(root) { |
| 45 | animation-duration: 0.3s; |
| 46 | animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); |
| 47 | } |
| 48 | ``` |
| 49 | |
| 50 | ## Scroll-Driven Animations |
| 51 | |
| 52 | - `animation-timeline: scroll()` (root scroller) or `view()` (element-in-viewport) |
| 53 | - Off-main-thread on Chrome stable + Safari 26 (2025) |
| 54 | - Firefox unsupported — pair with `prefers-reduced-motion` AND `duration:1ms` fallback |
| 55 | |
| 56 | ```css |
| 57 | @keyframes fade-up { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } |
| 58 | .reveal { animation: fade-up linear; animation-timeline: view(); animation-range: entry 0% cover 30%; } |
| 59 | ``` |
| 60 | |
| 61 | ## `@starting-style` (DOM-insert animation) |
| 62 | |
| 63 | Baseline 2026 — animates from explicit starting state to default state when element enters DOM. |
| 64 | |
| 65 | ```css |
| 66 | .toast { opacity: 1; transform: translateY(0); transition: opacity 0.3s, transform 0.3s; } |
| 67 | @starting-style { .toast { opacity: 0; transform: translateY(-10px); } } |
| 68 | ``` |
| 69 | |
| 70 | ## Container Scroll-State Queries (Baseline 2026) |
| 71 | |
| 72 | `@container scroll-state(stuck: top)` — replaces JS scroll-listener-based sticky styling. |
| 73 | |
| 74 | ```css |
| 75 | .nav { container-type: scroll-state; } |
| 76 | @container scroll-state(stuck: top) { .nav { box-shadow: 0 4px 12px rgba(0,0,0,0.4); } } |
| 77 | ``` |
| 78 | |
| 79 | ## Micro-Interactions (Tier 1 patterns) |
| 80 | |
| 81 | - **Button press** — `transform: scale(0.98)` on `:active`, 100ms |
| 82 | - **Hover** — `transform: translateY(-1px)` + color shift, 200ms |
| 83 | - **Focus-visible** — 3px brand-accent ring, 2px offset, 0ms (instant) |
| 84 | - **Tap** — haptic feedback via `navigator.vibrate(10)` if supported |
| 85 | - **Toggle** — animated check/cross morph via `<svg>` path interpolation |
| 86 | - **Loading** — pulse 1.2s ease-in-out infinite |
| 87 | |
| 88 | ## Stagger sequences (no JS needed) |
| 89 | |
| 90 | ```css |
| 91 | .list-item { animation: fade-up 0.4s ease-out backwards; } |
| 92 | /* OR use sibling-index() (Baseline 2026) */ |
| 93 | .list-item { transition-delay: calc((sibling-index() - 1) * 80ms); } |
| 94 | ``` |
| 95 | |
| 96 | ## Performance constraints |
| 97 | |
| 98 | - Animate `transform` + `opacity` ONLY on hot paths |
| 99 | - `will-change` sparingly (transform, opacity only when actually animated) |
| 100 | - Drop GPU layers after animation completes |
| 101 | - INP target ≤100ms per `_kernel/standards.md#cwv` — animations must not block input |
| 102 | |
| 103 | ## Interaction polish (every interactive element) |
| 104 | |
| 105 | Per `10-experience-and-design-system` § 4-state distinction: |
| 106 | |
| 107 | - Default → neutral |
| 108 | - Hover → underline-sweep + color shift + `translateY(-1px)` |
| 109 | - Focus-visible → 3px brand-accent ring 2px offset (distinct from hover) |
| 110 | - Active → `scale(0.98)` + immediate color confirm |
| 111 | |
| 112 | Audit gate: Playwright cycles each interactive through 4 states → diff ≥3px pixel-difference or fail. |
| 113 | |
| 114 | ## Banned motion |
| 115 | |
| 116 | - ❌ Uniform fade-in on every element (AI slop tell) |
| 117 | - ❌ |