Claude Cursor GitHub Copilot opencode Skill

motion-and-interaction-system

Meaning-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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download heymegabyte-claude-skills-11-motion-and-interaction-system-e7acb91.zip · 10 KB
Part of heymegabyte/claude-skills — 18 skills

Install

skills CLI npx skills add https://github.com/heymegabyte/claude-skills/tree/master/11-motion-and-interaction-system
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install heymegabyte-claude-skills@llmmart
Git git clone https://github.com/heymegabyte/claude-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole heymegabyte/claude-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

11 — Motion and Interaction System

Apply meaning-first CSS animations (scroll-driven, View Transitions, @starting-style) with mandatory prefers-reduced-motion on every animated surface.

Motion serves one of three purposes

  1. Feedback — confirm user action (button press, form submit, save)
  2. Continuity — preserve spatial context across state changes (page transition, modal open)
  3. Delight — express brand personality (hero parallax, signature reveal)

Anything else = AI slop. Cut it.

3-Tier Hierarchy

  • Tier 1 — Functional — feedback on every interaction (hover, focus, active, tap). Duration 100–200ms. Transform/opacity only.
  • Tier 2 — Choreographic — page transitions, modal entrance, section reveal. Duration 300–500ms. View Transitions or @starting-style.
  • Tier 3 — Cinematic — hero parallax, signature reveal, scroll-driven storytelling. Duration ≥600ms. Scroll-timeline.

Never stack 3 tiers on same surface. One cinematic per page.

Mandatory prefers-reduced-motion

EVERY animation MUST honor prefers-reduced-motion: reduce — snap to final state, never hide content.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

View Transitions API

  • Same-document SPA: document.startViewTransition(() => updateDOM())
  • Cross-document MPA: @view-transition { navigation: auto; } in CSS
  • Per-element: view-transition-name: hero-image; on persistent elements
  • Chrome 126+, Safari 18+, Firefox 144 (Oct 2025)
::view-transition-old(root), ::view-transition-new(root) {
  animation-duration: 0.3s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

Scroll-Driven Animations

  • animation-timeline: scroll() (root scroller) or view() (element-in-viewport)
  • Off-main-thread on Chrome stable + Safari 26 (2025)
  • Firefox unsupported — pair with prefers-reduced-motion AND duration:1ms fallback
@keyframes fade-up { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
.reveal { animation: fade-up linear; animation-timeline: view(); animation-range: entry 0% cover 30%; }

@starting-style (DOM-insert animation)

Baseline 2026 — animates from explicit starting state to default state when element enters DOM.

.toast { opacity: 1; transform: translateY(0); transition: opacity 0.3s, transform 0.3s; }
@starting-style { .toast { opacity: 0; transform: translateY(-10px); } }

Container Scroll-State Queries (Baseline 2026)

@container scroll-state(stuck: top) — replaces JS scroll-listener-based sticky styling.

.nav { container-type: scroll-state; }
@container scroll-state(stuck: top) { .nav { box-shadow: 0 4px 12px rgba(0,0,0,0.4); } }

Micro-Interactions (Tier 1 patterns)

  • Button press — transform: scale(0.98) on :active, 100ms
  • Hover — transform: translateY(-1px) + color shift, 200ms
  • Focus-visible — 3px brand-accent ring, 2px offset, 0ms (instant)
  • Tap — haptic feedback via navigator.vibrate(10) if supported
  • Toggle — animated check/cross morph via <svg> path interpolation
  • Loading — pulse 1.2s ease-in-out infinite

Stagger sequences (no JS needed)

.list-item { animation: fade-up 0.4s ease-out backwards; }
/* OR use sibling-index() (Baseline 2026) */
.list-item { transition-delay: calc((sibling-index() - 1) * 80ms); }

Performance constraints

  • Animate transform + opacity ONLY on hot paths
  • will-change sparingly (transform, opacity only when actually animated)
  • Drop GPU layers after animation completes
  • INP target ≤100ms per _kernel/standards.md#cwv — animations must not block input

Interaction polish (every interactive element)

Per 10-experience-and-design-system § 4-state distinction:

  • Default → neutral
  • Hover → underline-sweep + color shift + translateY(-1px)
  • Focus-visible → 3px brand-accent ring 2px offset (distinct from hover)
  • Active → scale(0.98) + immediate color confirm

Audit gate: Playwright cycles each interactive through 4 states → diff ≥3px pixel-difference or fail.

Banned motion

  • ❌ Uniform fade-in on every element (AI slop tell)
  • ❌ Parallax on every section (one cinematic per page)
  • ❌ Spinning loaders that don't progress
  • ❌ Auto-playing video with sound
  • ❌ Carousel auto-rotate (manual swipe only — accessibility)
  • ❌ Scroll-jacking that breaks browser back button
  • ❌ Animations that block input (INP >100ms)
  • ❌ Easter eggs on critical conversion paths

See submodules: view-transitions.md, scroll-driven.md, micro-interactions.md, reduced-motion.md, build-breaking-rules.md.

Files (claude-skills)
  • build-breaking-rules.md 18 KB
    ---
    name: "11 build-breaking motion rules"
    description: "Universal motion/interaction gates: stat counter roll-in, click ripple (no cursor follower), 4-state interactive (active/hover/focus/focus-visible) + universal underline-hover (single underline only), no-layout-shift image hover, no-white-flash card hover, View Transitions cross-fade, animate.css entrance, anti-FOUC fadeIn. Migrated verbatim from rules/always.md 2026-05-03."
    metadata:
      version: "1.0.0"
      updated: "2026-05-03"
      effort: "high"
      context: "fork"
    license: "Rutgers"
    compatibility:
      claude-code: ">=2.0.0"
      agentskills: ">=1.0.0"
    ---
    
    # 11 — Build-Breaking Motion + Interaction Rules
    
    Migrated from `~/.claude/rules/always.md` 2026-05-03.
    
    ## Every stat block
    
    `150+ Publications`, `30+ Years`, `$2.4M raised`:
    
    - IntersectionObserver + rAF roll-in counter ≤1.6s ease-out; respects `prefers-reduced-motion`
    - `+` / `%` / `x` suffix renders OUTSIDE animated digit node
    
    ## Every site (interactive 2 — 4 distinct states + universal underline-hover)
    
    All 4 distinct + animated states required per button/link/input/tile: `:active`, `:hover`, `:focus`, `:focus-visible`. Build gate: visual-qa subagent verifies via DOM snapshot at 6 breakpoints.
    
    **Tile-as-link** — entire tile is click target with hover lift, not just inner chip.
    
    ### Universal underline-hover for body+heading `<a>` (***EXACTLY ONE UNDERLINE — NEVER TWO — UNIVERSAL — BUILD-BREAKING***)
    
    - Block MUST live at END of global stylesheet, OUTSIDE `@layer components` — Tailwind v4 layer order (base < components < utilities) means `text-decoration:none` inside components LOSES to Tailwind's `underline` utility, producing double-underline (fail)
    - Auto-apply selectors set `text-decoration:none !important` + `position:relative` + transition ONLY — NEVER set `color` (hardcoded color overrides Tailwind parent classes, causing invisible dark-on-dark links)
    - `::after` MUST use `background:currentColor` (NEVER a hardcoded brand var) — sweep matches link's text color in any context
    
    ```css
    .underline-hover, .blog-paragraph a, main p a:not([class*="btn-"]):not([data-no-underline]):not(:has(img)):not(:has(svg)) {
      position:relative;
      text-decoration:none !important;
      transition:color .2s ease,opacity .2s ease
    }
    .underline-hover::after, ...::after {
      content:""; position:absolute; z-index:1;
      left:51%; right:51%; bottom:-2px;
      background:currentColor; height:1px;
      opacity:.6;
      transition:left .3s ease-out,right .3s ease-out,opacity .2s ease;
      pointer-events:none
    }
    .underline-hover:hover::after, .underline-hover:focus-visible::after {
      left:0; right:0; opacity:1
    }
    ```
    
    ## Every desktop site (***CLICK RIPPLE ONLY — UNIVERSAL — megabyte.space reference***)
    
    Desktop-only: `(min-width:768px) and (pointer:fine)` AND NOT `prefers-reduced-motion:reduce`.
    
    - **NEVER** hide native cursor; **NO** cursor-ring/follower/dot/halo (removed 2026-05-02)
    - `mousedown` spawns `<span class="cursor-ripple">` at click coords: 8px→60px expand, `border:1.5px solid var(--brand-accent)`, `position:fixed`, `pointer-events:none`, `z-index:9999`, `mix-blend-mode:difference`, opacity 0.6→0, 0.6s ease-out, removed on `animationend`
    - Touch/`pointer:coarse` AND reduced-motion BOTH skip the ENTIRE system (no init)
    - Template ships `src/lib/cursor.ts` + global CSS (ripple-only, ring code deleted) — never hand-rolled per site
    
    Build gate `validate-cursor.mjs`: assert NO `.cursor-ring` element in DOM, AND `body{cursor:none}` rule absent, AND ripple spawns on mousedown.
    
    ## Every image hover (***NO LAYOUT-AFFECTING PROPERTIES — UNIVERSAL — BUILD-BREAKING***)
    
    `<img>` / `<picture>` / `<svg>` `:hover` / `:focus` MUST NOT mutate `border | outline | padding | margin | width | height | border-width | inset | top | left | right | bottom`.
    
    Allowed: `transform` (scale/translate), `filter` (brightness/saturate/blur), `box-shadow` (incl. `inset 0 0 0 2px var(--brand-accent)`), `opacity`. Default state must reserve final box (e.g. always-on `border:2px solid transparent`).
    
    Build gate `validate-image-hover.mjs`: triggers `:hover` on every image, samples bounding-rect before+after — any dimension shift >0px = fail; CSS grep rejects `img:hover{border|outline|padding|margin|width|height: ...}`.
    
    ## Every card hover (***NO WHITE-FLASH ON FIRST HOVER — UNIVERSAL — BUILD-BREAKING***)
    
    Card components MUST NOT flash white/transparent on first hover. Root cause: hover transitions on `background-color` or `box-shadow` without explicit FROM-state cause browser to interpolate from `initial` (transparent/none).
    
    ### Required pattern
    
    ```css
    .card {
      background-color: var(--card-bg);
      box-shadow: 0 1px 2px rgba(0,0,0,0.05);
    }
    .card:hover {
      background-color: var(--card-bg-hover);
      box-shadow: 0 8px 24px rgba(0,0,0,0.12);
      transform: translateY(-3px);
    }
    ```
    
    - `will-change: transform, box-shadow` only when hover frequency justifies
    - Prefer `transform:translateY` over `top/margin` for lift; prefer `opacity` change on pseudo-element overlay over `background-color` change
    
    Validator (`validate-card-hover-no-flicker.mjs`): Playwright records 30fps video of cursor entering card; any single frame >20% brighter than rest-state followed by darker frames = white-flicker fail.
    
    ## Every page transition
    
    View Transitions API cross-fade 220ms ease-out, `html{background-color:var(--bg-primary)}` — NEVER allow a white frame between routes.
    
    ## Every entrance animation
    
    `animate.css` bundled globally (NOT CDN, no FOUT); prefer its classes (`animate__fadeInUp` / `animate__zoomIn` / `animate__slideInLeft`) for entrance/exit. Custom `@keyframes` only when `animate.css` insufficient.
    
    ## Every site (***ADVANCED BROWSER API INTEGRATION — MINIMUM 3 APIS BEYOND DOM — UNIVERSAL — BUILD-BREAKING***)
    
    Every generated site MUST implement ≥3 advanced browser APIs providing genuine interactive value.
    
    ### Mandatory baseline (all sites)
    
    1. **Web Audio API** — UI feedback sounds on primary CTA: button click 80ms 220Hz→440Hz; hover 20ms 880Hz 0.02 gain; total audio budget ≤200KB; opt-in respects `prefers-reduced-motion` + localStorage audio-mute toggle; `AudioContext` lazy-init on first user gesture (iOS); `gainNode.gain.value = 0.03`; implementation: `src/lib/audio.ts`
    2. **IntersectionObserver v2** — upgrade to `trackVisibility:true, delay:100` for above-fold lazy-load of heavy assets (video, Three.js canvas, podcast player)
    3. **Pointer Events API** — replace all `mouseenter`/`mouseleave` with `pointerenter`/`pointerleave`; card tilt on `pointermove`: `transform:perspective(800px) rotateX(${-y*6}deg) rotateY(${x*6}deg)` (x/y normalized to [-1,1]); disable when `prefers-reduced-motion`
    
    ### Required 4th API choice (pick by site type)
    
    - **WebGL/Three.js** — tech/SaaS: hero particle field (Three.js instanced mesh, 2000 particles, `requestAnimationFrame`, GPU-composited)
    - **Web Speech API** — service businesses: "Press / to search" voice command on desktop, graceful degradation when unavailable
    - **Vibration API** — mobile-local-business: 40ms pulse on booking confirmation, 10ms on nav tap, 0ms on error
    - **Web Share API** — content/blog: `navigator.share({title,text,url})` on mobile, fallback to `Clipboard API` copy
    - **Payment Request API** — e-commerce/SaaS: native browser payment sheet on "Buy Now" on supported browsers
    - **Gamepad API** — portfolio/interactive: konami-code easter egg (↑↑↓↓←→←→BA) triggering brand-moment animation + particle explosion
    
    Validator (`validate-advanced-apis.mjs`): grep dist/ JS bundle for `AudioContext|webkitAudioContext` + one of `PointerEvent|pointermove|pointerenter` + one type-specific API — any missing = fail.
    
    ## Every entrance animation (***DURATION CAP 600ms + TRANSFORM/OPACITY ONLY — UNIVERSAL — BUILD-BREAKING***)
    
    All entrance/exit/reveal animations MUST:
    
    - (a) Complete within 600ms total
    - (b) Animate ONLY `transform` and `opacity` (GPU-composited) — NEVER `height`, `margin`, `padding`, `font-size`, `width`, `left`, `top`, `right`, `bottom`
    
    - Allowed: `transform:translateY(20px) scale(0.96)` + `opacity:0→1`
    - Forbidden: `height:0→auto` transitions (use `max-height` with fixed px + `overflow:hidden` if unavoidable)
    - Stagger delay max 80ms per sibling (3 items = 0/80/160ms — never 0/200/400ms)
    
    Build gate `validate-animation-timing.mjs`: parse dist/ CSS for `@keyframes` + `transition` + `animation` duration values — fail any individual step >600ms.
    
    ## Every font load (***FONT-DISPLAY SWAP + METRIC MATCHING — UNIVERSAL — BUILD-BREAKING***)
    
    - All web fonts MUST use `font-display:swap` in `@font-face` to prevent FOIT
    - `<link rel="preload" as="font" type="font/woff2" crossorigin href="/fonts/{primary}.woff2">` in `<head>` BEFORE any `<link rel="stylesheet">`
    - Google Fonts: use `?display=swap` param
    - Fallback fonts MUST use metric-adjusted system fonts (`size-adjust` + `ascent-override` + `descent-override` + `line-gap-override`) to minimize FOUT CLS
    
    ```css
    @font-face { font-family: 'Inter-Fallback'; src: local('Arial'); size-adjust: 100.06%; ascent-override: 90%; descent-override: 22%; line-gap-override: 0%; }
    ```
    
    Then `font-family: 'Inter', 'Inter-Fallback', sans-serif`.
    
    Build gate: Lighthouse `font-display:optional|swap` audit — any FOIT-causing font = fail; CLS from font-swap must be <0.05.
    
    ## Every site (anti-FOUC + universal in-viewport fadeIn — ***NO TEXT JUMPING ON LOAD***)
    
    Every module MUST fadeIn ONCE when entering viewport (IntersectionObserver + animate.css `animate__fadeInUp animate__faster`, single trigger).
    
    - Text MUST start at `opacity:0` (`@starting-style` OR `.reveal-init{opacity:0;transform:translateY(20px)}`) until JS attaches observer — NEVER flash visible then jump
    - INLINE `<script>` in `<head>` (BEFORE any CSS `<link>`, blocking, not defer/async) adds `js-reveal-active` to `<html>` before first paint
    - CSS: `html.js-reveal-active .reveal:not(.is-visible){opacity:0;transform:translateY(20px)}`; JS toggles `.is-visible` on `entry.isIntersecting`
    - `prefers-reduced-motion:reduce` — observer MUST check `matchMedia(...).matches` and call `el.classList.add('is-visible')` synchronously on ALL reveal elements on DOMContentLoaded — NEVER leave content hidden for reduced-motion users
    - Safety timeout: after 1500ms, if ANY `.reveal:not(.is-visible)` exists, auto-add `is-visible` to ALL (catches IntersectionObserver failures, SSR, noJS)
    
    Validator: post-deploy E2E screenshots at t=0, t=200ms, t=2s — assert no text element has opacity <0.1 after 2s; assert no CLS spike between frames.
    
    ## Every `<img>` (***LAZY LOADING + DIMENSIONS + FETCHPRIORITY — UNIVERSAL — BUILD-BREAKING***)
    
    Every `<img>` in dist/ MUST have:
    
    - (a) `width` and `height` attributes set to natural dimensions (prevents CLS)
    - (b) `loading="lazy"` on every non-ATF image
    - (c) ATF images: `loading="eager" fetchpriority="high"` — NEVER `loading="lazy"` on hero/above-fold
    - (d) `decoding="async"` on all non-hero images
    
    ### Required shapes
    
    - **Hero** — `<img src="/assets/hero.webp" width="1920" height="1080" loading="eager" fetchpriority="high" decoding="sync" alt="…">`
    - **Card** — `<img src="/assets/card.webp" width="800" height="600" loading="lazy" decoding="async" alt="…">`
    
    WebP primary with `<picture>` AVIF source:
    
    ```html
    <picture><source type="image/avif" srcset="…"><img …></picture>
    ```
    
    Every hero MUST have `srcset` at 640/1280/1920:
    
    ```html
    srcset="/assets/hero-640.webp 640w, /assets/hero-1280.webp 1280w, /assets/hero-1920.webp 1920w" sizes="100vw"
    ```
    
    Validator (`validate-img-attributes.mjs`): any `<img>` without `width` = fail; without `loading` = fail; ATF `<img>` with `loading="lazy"` = fail; ATF `<img>` without `fetchpriority="high"` = fail.
    
    ## Every hero `<video>` (***AUTOPLAY ATTRIBUTES + PRELOAD METADATA — UNIVERSAL — BUILD-BREAKING***)
    
    Every background/hero `<video>` MUST have exactly: `autoplay muted loop playsinline preload="metadata"` — all five.
    
    - `autoplay` without `muted` is browser-blocked; `playsinline` prevents iOS fullscreen hijack; `preload="metadata"` saves bandwidth
    - NEVER `preload="auto"` on background videos
    - ALWAYS include `poster="/assets/video-poster.webp"`
    - `aria-hidden="true"` when purely decorative
    
    When `prefers-reduced-motion:reduce`, pause via JS:
    
    ```js
    const mql = matchMedia("(prefers-reduced-motion: reduce)");
    if (mql.matches) video.pause()
    ```
    
    MUST be self-hosted in R2 as `.mp4` (H.264 baseline, ≤8MB) + `.webm` (VP9):
    
    ```html
    <source src="/assets/hero.webm" type="video/webm">
    <source src="/assets/hero.mp4" type="video/mp4">
    ```
    
    Validator (`validate-video-attributes.mjs`): every `<video>` — assert `muted`, `playsinline`, `preload` is `metadata` or `none` (never `auto`), `poster` set.
    
    ## Every third-party script (***DEFER OR ASYNC — UNIVERSAL — BUILD-BREAKING***)
    
    Every external `<script src>` in `<head>` MUST use `defer` or `async`.
    
    - `defer` for DOM-dependent ordered scripts (analytics, tag managers)
    - `async` for independent scripts (chat widgets, pixels)
    - GTM: `<script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXX">`
    - `type="module"` scripts are implicitly deferred
    - EXCEPTION: anti-FOUC inline `<script>` (no src) MUST remain synchronous and blocking — all other inline scripts referencing DOM MUST move to `DOMContentLoaded`
    
    Build gate `validate-script-defer.mjs`: every `<script src>` without `defer` | `async` | `type="module"` outside FOUC whitelist = fail.
    
    ## Every site (***CRITICAL CSS INLINED ≤14KB — UNIVERSAL — BUILD-BREAKING***)
    
    ATF CSS MUST be inlined in `<style>` within `<head>` before any `<link rel="stylesheet">` — max 14KB compressed.
    
    - Build step runs `critical` npm package (or `penthouse`) against each route URL → inlines in `<style id="critical-css">`
    - Full stylesheet `<link>` gets `media="print" onload="this.media='all'"` + `<noscript><link rel="stylesheet" href="/styles.css"></noscript>` fallback
    - Critical CSS must include: `body`/`html`, typography base, nav background, hero section, H1 styles, above-fold CTA button
    - NEVER inline ALL CSS; NEVER skip critical inlining
    
    Build gate: Lighthouse "Eliminate render-blocking resources" — zero `<link rel="stylesheet">` in critical path.
    
    Validator (`validate-critical-css.mjs`): assert `<head>` contains `<style id="critical-css">` with `length > 500` AND full stylesheet `<link>` has `media="print"`.
    
    ## Every site (***LATEST-TECH FLEX #1 — VIEW TRANSITIONS ON EVERY NAV — UNIVERSAL — BUILD-BREAKING***)
    
    Every site MUST use `document.startViewTransition()` for ALL internal navigation — never page reloads or instant swaps.
    
    Per-route: `<meta name="view-transition" content="same-origin">` + `@view-transition{navigation:auto}` CSS.
    
    ### Required treatments
    
    - Cross-fade for unrelated routes (default)
    - Shared-element morph for hero→detail (`view-transition-name:hero-<slug>` on source + destination images)
    - Slide-from-right for forward nav; slide-from-left for back nav
    - Fallback (Safari pre-18.2): instant swap with `@supports not (view-transition-name:x)`
    
    Validator (`validate-view-transitions.mjs`): assert `@view-transition` CSS rule + ≥2 `view-transition-name` declarations + `<meta name="view-transition">` in `<head>`.
    
    ## Every site (***LATEST-TECH FLEX #2 — SCROLL-DRIVEN ANIMATIONS NATIVE — UNIVERSAL — BUILD-BREAKING***)
    
    Every site MUST use CSS-native `animation-timeline:view()` AND `animation-timeline:scroll()` for scroll-progress bars, parallax sections, sticky hero zoom-outs, and section reveals — NOT JS IntersectionObserver where CSS suffices.
    
    JS-driven scroll animations permitted ONLY for: (a) audio-reactive heroes, (b) WebGL particle fields, (c) interactive scrubbers requiring exact position.
    
    Fallback (Safari): IntersectionObserver behind `@supports not (animation-timeline:view())`.
    
    Validator (`validate-scroll-driven-css.mjs`): assert `animation-timeline:` CSS rules in `dist/**/*.css` ≥3 occurrences AND scroll-progress indicator on every long-form page (`<progress data-scroll-progress>` OR CSS-only fixed bar).
    
    ## Every site (***LATEST-TECH FLEX #3 — CSS @SCOPE FOR COMPONENT ISOLATION — UNIVERSAL — BUILD-BREAKING***)
    
    Every component CSS MUST use `@scope` to isolate selectors to component subtree; cascade layers (`@layer`) in tandem for predictable specificity: `@layer reset,base,components,utilities`.
    
    ```css
    @scope (.card) to (.card-content > *) {
      h2 { ... }
      p { ... }
    }
    ```
    
    Validator (`validate-css-scope.mjs`): assert ≥3 `@scope` blocks in `dist/**/*.css` AND `@layer` declarations at top of main stylesheet.
    
    ## Every site (***LATEST-TECH FLEX #4 — POPOVER + ANCHOR POSITIONING NATIVE — UNIVERSAL — BUILD-BREAKING***)
    
    Every site with tooltips/dropdowns/menus/modals MUST use native Popover API (`<button popovertarget="menu">` + `<div popover="auto">`) AND native CSS anchor positioning (`position-anchor:--trigger;inset-area:bottom span-left`) — NOT third-party Floating UI/Popper.js where Popover suffices.
    
    Modals via `<dialog>` + `showModal()` (native focus trap + Esc handling).
    
    Validator (`validate-popover-api.mjs`): assert dist HTML contains ≥2 `popovertarget=` attributes AND `position-anchor` CSS rule present OR no third-party `floating-ui`/`popper` script tags.
    
    ## Every site (***LATEST-TECH FLEX #5 — SPECULATION RULES API — UNIVERSAL — BUILD-BREAKING***)
    
    Every site MUST ship Speculation Rules block in `<head>`:
    
    ```html
    <script type="speculationrules">
    {
      "prerender": [{"where": {"href_matches": "/about|/services|/contact|/pricing"}}],
      "prefetch": [{"where": {"href_matches": "/*"}, "eagerness": "moderate"}]
    }
    </script>
    ```
    
    - Nav links + hero CTA destinations → `prerender`; footer + body links → `prefetch`
    - Chromium-only; graceful no-op in Safari/Firefox
    
    Validator (`validate-speculation-rules.mjs`): assert `<script type="speculationrules">` in every route HTML + valid JSON + ≥1 prerender + ≥1 prefetch rule.
    
    ## Every site (***LATEST-TECH FLEX #6 — HTML INVOKER COMMANDS — UNIVERSAL — BUILD-BREAKING***)
    
    Every site with ≥1 `<dialog>` OR `<div popover>` MUST use HTML invoker commands on corresponding buttons:
    
    ```html
    <button commandfor="filter-modal" command="show-modal">Filter</button>
    ```
    
    Validator (`validate-invoker-commands.mjs`): assert when `<dialog>` OR `<div popover>` exists, ≥1 corresponding `<button command="...">` exists.
    
  • SKILL.md 5.4 KB
    ---
    name: "motion-and-interaction-system"
    description: "Meaning-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."
    metadata:
      version: "2.1.0"
      updated: "2026-05-03"
      effort: "medium"
      model: "sonnet"
    license: "Rutgers"
    compatibility:
      claude-code: ">=2.0.0"
      agentskills: ">=1.0.0"
    submodules:
      - build-breaking-rules.md
    priority: 3
    pack: "design"
    stage: beta
    triggers:
      - "motion"
      - "animation"
      - "transition"
    paths:
      - "org:website_build"
    ---
    
    # 11 — Motion and Interaction System
    
    Apply meaning-first CSS animations (scroll-driven, View Transitions, `@starting-style`) with mandatory `prefers-reduced-motion` on every animated surface.
    
    ## Motion serves one of three purposes
    
    1. **Feedback** — confirm user action (button press, form submit, save)
    2. **Continuity** — preserve spatial context across state changes (page transition, modal open)
    3. **Delight** — express brand personality (hero parallax, signature reveal)
    
    Anything else = AI slop. Cut it.
    
    ## 3-Tier Hierarchy
    
    - **Tier 1 — Functional** — feedback on every interaction (hover, focus, active, tap). Duration 100–200ms. Transform/opacity only.
    - **Tier 2 — Choreographic** — page transitions, modal entrance, section reveal. Duration 300–500ms. View Transitions or `@starting-style`.
    - **Tier 3 — Cinematic** — hero parallax, signature reveal, scroll-driven storytelling. Duration ≥600ms. Scroll-timeline.
    
    Never stack 3 tiers on same surface. One cinematic per page.
    
    ## Mandatory `prefers-reduced-motion`
    
    EVERY animation MUST honor `prefers-reduced-motion: reduce` — snap to final state, never hide content.
    
    ```css
    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
      }
    }
    ```
    
    ## View Transitions API
    
    - Same-document SPA: `document.startViewTransition(() => updateDOM())`
    - Cross-document MPA: `@view-transition { navigation: auto; }` in CSS
    - Per-element: `view-transition-name: hero-image;` on persistent elements
    - Chrome 126+, Safari 18+, Firefox 144 (Oct 2025)
    
    ```css
    ::view-transition-old(root), ::view-transition-new(root) {
      animation-duration: 0.3s;
      animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
    }
    ```
    
    ## Scroll-Driven Animations
    
    - `animation-timeline: scroll()` (root scroller) or `view()` (element-in-viewport)
    - Off-main-thread on Chrome stable + Safari 26 (2025)
    - Firefox unsupported — pair with `prefers-reduced-motion` AND `duration:1ms` fallback
    
    ```css
    @keyframes fade-up { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
    .reveal { animation: fade-up linear; animation-timeline: view(); animation-range: entry 0% cover 30%; }
    ```
    
    ## `@starting-style` (DOM-insert animation)
    
    Baseline 2026 — animates from explicit starting state to default state when element enters DOM.
    
    ```css
    .toast { opacity: 1; transform: translateY(0); transition: opacity 0.3s, transform 0.3s; }
    @starting-style { .toast { opacity: 0; transform: translateY(-10px); } }
    ```
    
    ## Container Scroll-State Queries (Baseline 2026)
    
    `@container scroll-state(stuck: top)` — replaces JS scroll-listener-based sticky styling.
    
    ```css
    .nav { container-type: scroll-state; }
    @container scroll-state(stuck: top) { .nav { box-shadow: 0 4px 12px rgba(0,0,0,0.4); } }
    ```
    
    ## Micro-Interactions (Tier 1 patterns)
    
    - **Button press** — `transform: scale(0.98)` on `:active`, 100ms
    - **Hover** — `transform: translateY(-1px)` + color shift, 200ms
    - **Focus-visible** — 3px brand-accent ring, 2px offset, 0ms (instant)
    - **Tap** — haptic feedback via `navigator.vibrate(10)` if supported
    - **Toggle** — animated check/cross morph via `<svg>` path interpolation
    - **Loading** — pulse 1.2s ease-in-out infinite
    
    ## Stagger sequences (no JS needed)
    
    ```css
    .list-item { animation: fade-up 0.4s ease-out backwards; }
    /* OR use sibling-index() (Baseline 2026) */
    .list-item { transition-delay: calc((sibling-index() - 1) * 80ms); }
    ```
    
    ## Performance constraints
    
    - Animate `transform` + `opacity` ONLY on hot paths
    - `will-change` sparingly (transform, opacity only when actually animated)
    - Drop GPU layers after animation completes
    - INP target ≤100ms per `_kernel/standards.md#cwv` — animations must not block input
    
    ## Interaction polish (every interactive element)
    
    Per `10-experience-and-design-system` § 4-state distinction:
    
    - Default → neutral
    - Hover → underline-sweep + color shift + `translateY(-1px)`
    - Focus-visible → 3px brand-accent ring 2px offset (distinct from hover)
    - Active → `scale(0.98)` + immediate color confirm
    
    Audit gate: Playwright cycles each interactive through 4 states → diff ≥3px pixel-difference or fail.
    
    ## Banned motion
    
    - ❌ Uniform fade-in on every element (AI slop tell)
    - ❌ Parallax on every section (one cinematic per page)
    - ❌ Spinning loaders that don't progress
    - ❌ Auto-playing video with sound
    - ❌ Carousel auto-rotate (manual swipe only — accessibility)
    - ❌ Scroll-jacking that breaks browser back button
    - ❌ Animations that block input (INP >100ms)
    - ❌ Easter eggs on critical conversion paths
    
    ## See submodules: view-transitions.md, scroll-driven.md, micro-interactions.md, reduced-motion.md, build-breaking-rules.md.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related