diff --git a/.cursor/plans/motion_presets_llm_rules.plan.md b/.cursor/plans/motion_presets_llm_rules.plan.md
deleted file mode 100644
index 7ce5c8e4..00000000
--- a/.cursor/plans/motion_presets_llm_rules.plan.md
+++ /dev/null
@@ -1,729 +0,0 @@
----
-name: LLM Preset Rules
-overview: Single source of truth for generating and maintaining all preset reference files.
-todos: []
-isProject: false
----
-
-# LLM Rules for Motion Presets
-
-This file is the **single source of truth** for generating all preset reference files. Every guideline, table, and parameter standard in the generated files originates here.
-
-## Table of Contents
-
-- [Generated Files](#generated-files)
-- [Skills Compatibility](#skills-compatibility)
-- [Terminology](#terminology)
-- [Preset Registry](#preset-registry)
-- [Key Constraints](#key-constraints)
-- [Parameter Standards](#parameter-standards)
-- [Optional Parameters](#optional-parameters)
-- [Accessibility](#accessibility)
-- [Selection Tables](#selection-tables)
-- [Intensity Value Guide](#intensity-value-guide)
-- [Preset Entry Format](#preset-entry-format)
-- [Regeneration Steps](#regeneration-steps)
-
-## Generated Files
-
-```text
-packages/motion-presets/rules/presets/
-├── presets-main.md # Generated: entry point (<500 lines) — decision flow, categories, standards, selection, a11y
-├── entrance-presets.md # Generated: full entrance preset params, examples, optional params, intensity
-├── scroll-presets.md # Generated: full scroll preset params, examples, optional params, intensity
-├── ongoing-presets.md # Generated: full ongoing preset params, examples, intensity
-└── mouse-presets.md # Generated: full mouse preset params, examples, intensity, mobile notes
-```
-
-### What Goes Where
-
-| Source Section (this file) | Generates Into |
-| ------------------------------------------------- | ----------------------- |
-| Terminology | presets-main.md |
-| Key Constraints (categories, triggers, combining) | presets-main.md |
-| Parameter Standards | presets-main.md |
-| Selection Tables | presets-main.md |
-| Accessibility | presets-main.md |
-| Preset Registry | presets-main.md (lists) |
-| Preset Entry Format + source code | {category}-presets.md |
-| Optional Parameters | {category}-presets.md |
-| Intensity Value Guide | {category}-presets.md |
-
----
-
-## Skills Compatibility
-
-The generated files are structured for future conversion to an Agentic Skill. When generating or editing these files, follow these conventions so they can be moved with minimal changes:
-
-### Frontmatter
-
-Every generated file must have YAML frontmatter with at least `name` and `description`. The `description` should be written in third person and include both WHAT the file does and WHEN an agent should read it.
-
-```yaml
----
-name: lowercase-with-hyphens (max 64 chars)
-description: Third-person description with trigger terms. Max 1024 chars.
----
-```
-
-When converting to a real skill, `presets-main.md` becomes `SKILL.md` and its `description` becomes the skill discovery text.
-
-### Structure Rules
-
-- **Main entry file** (`presets-main.md` / future `SKILL.md`): under 500 lines
-- **Reference files**: linked one level deep from the main file, no further nesting
-- **Heading hierarchy**: `#` (title) → `##` (sections) → `###` (subsections) — no skipped levels
-- **Progressive disclosure**: essential info in the main file, detailed reference in separate files
-- **TOC**: include a table of contents in every file
-- **Consistent terminology**: "preset" for selection, "effect" for runtime, "animation" for visual motion
-- **No time-sensitive information**: avoid dates, version-specific caveats
-
-### Future Conversion Checklist
-
-To convert to a real Cursor Skill:
-
-1. Create the skill in the right "skills" folder
-2. Copy `presets-main.md` → `SKILL.md`
-3. Copy `{category}-presets.md` files alongside it
-4. Verify `SKILL.md` description has trigger terms for agent discovery
-5. Remove `category` field from frontmatter (not needed in skills)
-6. Verify all internal links still resolve
-
----
-
-## Terminology
-
-| Term | Meaning |
-| ------------- | --------------------------------------------------------------------------------------- |
-| **Effect** | Interact's term for an operation applied to an element (animation, custom effect, etc.) |
-| **Preset** | A pre-built, named effect configuration from this library (e.g., `FadeIn`, `BounceIn`) |
-| **Animation** | The actual visual motion that runs in the browser (CSS or WAAPI) |
-
-A preset is a named effect. "Preset" is used when talking about selection and configuration; "effect" when talking about the Interact runtime; "animation" when referring to the visual motion or CSS/WAAPI mechanism.
-
----
-
-## Preset Registry
-
-A list of the presets present in the project. Before continuing, make sure this list is aligned with `packages/motion-presets/src/library` and update accordingly.
-
-Descriptions marked with **(designer)** are approved by design and should be used as-is in generated files. All other descriptions are derived and should follow the same style.
-
-### Excluded Presets
-
-The following presets exist in the library but should **not** be documented in the generated rules:
-
-- `CustomMouse` — fully custom callback, not a configurable preset
-- `SpinMouse` — excluded by design
-- `BounceMouse` — excluded by design
-
-### Entrance Presets
-
-| Preset | Description |
-| ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |
-| FadeIn | Element fades in smoothly from fully transparent to fully opaque. |
-| ArcIn | **(designer)** Element enters along a 3D arc path, rotating into view with depth motion. |
-| BlurIn | **(designer)** Element transitions from blurred to sharp while fading in. |
-| BounceIn | Element bounces into place from a direction with an elastic multi-step curve. |
-| CurveIn | **(designer)** Element curves in with a 180° rotation and depth motion in a 3D space, creating a swinging arc entrance. |
-| DropIn | **(designer)** Element shrinks down from a larger size to its final scale. |
-| ExpandIn | Element expands from a point in a given direction, scaling from small to full size with a fade-in. |
-| FlipIn | Element flips into view with a 3D rotation around the X or Y axis. |
-| FloatIn | Element drifts gently into place from a direction with a fade-in. |
-| FoldIn | Element unfolds from an edge, rotating around an axis at the edge as if hinged. |
-| GlideIn | **(designer)** Element glides in smoothly from off-screen along a direction. |
-| RevealIn | Element is progressively revealed by an expanding clip-path from one edge. |
-| ShapeIn | Element appears through an expanding geometric clip-path shape. |
-| ShuttersIn | Element is revealed through multiple shutter-like strips that open in sequence. |
-| SlideIn | Element slides in from one side while being revealed with a clip-path mask. |
-| SpinIn | Element spins into view while scaling from small to full size. |
-| TiltIn | Element tilts in from the side with 3D rotation and a clip-path reveal. |
-| TurnIn | Element rotates into view around a corner pivot point. |
-| WinkIn | **(designer)** Element winks into view by expanding from its horizontal or vertical center, while being revealed with a clip-path. |
-
-### Scroll Presets
-
-| Preset | Description |
-| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| ArcScroll | Element rotates along a 3D arc as it scrolls into or out of view. |
-| BlurScroll | Element blurs or unblurs as it scrolls through the viewport. |
-| FadeScroll | Element fades in or out based on scroll position. |
-| FlipScroll | Element performs a 3D flip rotation as it scrolls. |
-| GrowScroll | **(designer)** Element scales up from a direction as it scrolls into or out of view. |
-| MoveScroll | Element translates along an angle for a given distance as it scrolls. |
-| PanScroll | **(designer)** Horizontal panning tied to scroll. |
-| ParallaxScroll | Element moves at a different speed than the scroll, creating a depth illusion. |
-| RevealScroll | Element is progressively revealed from an edge via clip-path as it scrolls. |
-| ShapeScroll | Element is revealed through an expanding geometric clip-path shape on scroll. |
-| ShrinkScroll | **(designer)** Element shrinks toward a direction as it scrolls into or out of view, the inverse of GrowScroll. |
-| ShuttersScroll | **(designer)** Element is revealed through staggered shutter-like strips that open on scroll in. When scrolling out, the element disappears with the same animation in reverse. |
-| SkewPanScroll | Element pans horizontally with a skew distortion as it scrolls. |
-| SlideScroll | Element slides in from an edge with a clip-path reveal as it scrolls. |
-| Spin3dScroll | Element performs a 3D spin with rotation on multiple axes as it scrolls. |
-| SpinScroll | Element spins (2D rotation) with optional scale change as it scrolls. |
-| StretchScroll | Element stretches vertically with scaleY increasing while scaleX decreases, with an opacity transition. |
-| TiltScroll | **(designer)** Element tilts in 3D and perspective, with optional parallax vertical movement as it scrolls into or out of view. |
-| TurnScroll | Element pans in from off-screen while turning (rotating) as it scrolls. |
-
-### Ongoing Presets
-
-| Preset | Description |
-| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| Bounce | Element bounces up and down with a natural multi-step curve, like a ball settling. |
-| Breathe | Element gently moves back and forth along an axis, like a breathing motion. |
-| Cross | **(designer)** Element moves across the screen from side to side, horizontally or vertically, until reaching the edge of the view and repeats. |
-| DVD | **(designer)** Element bounces diagonally off the viewport edges like a DVD screensaver logo. No configurable parameters — uses viewport dimensions to calculate bounce paths. |
-| Flash | Element blinks by rapidly cycling opacity from visible to invisible and back. |
-| Flip | Element continuously flips with a full 360° 3D rotation. |
-| Fold | Element folds at an edge using 3D rotation, like a page turning back and forth. |
-| Jello | Element wobbles with a skew-based jello-like deformation. |
-| Poke | **(designer)** Element makes two short, sharp translates in a direction back and forth, like being poked. |
-| Pulse | **(designer)** Element pulses by subtly scaling up and down. |
-| Rubber | Element stretches non-uniformly on X and Y axes, creating a rubber-band wobble. |
-| Spin | Element rotates continuously around its center. |
-| Swing | Element swings like a pendulum from a pivot at one edge. |
-| Wiggle | Element shakes with combined rotation and vertical translation. |
-
-### Mouse Presets
-
-| Preset | Description |
-| ------------ | -------------------------------------------------------------------------------------------------------------------------- |
-| AiryMouse | Element floats and rotates gently following the cursor, creating an airy, weightless feel. |
-| BlobMouse | **(designer)** Element translates and scales non-uniformly following the cursor, creating a heavy liquid-like deformation. |
-| BlurMouse | Element translates, tilts in 3D, scales, and blurs based on distance from the cursor. |
-| BounceMouse | _(excluded from generated rules)_ Element follows the cursor with an elastic, bouncy motion. |
-| CustomMouse | _(excluded from generated rules)_ Fully custom callback effect. |
-| ScaleMouse | Element translates and scales uniformly following the cursor. |
-| SkewMouse | Element translates and skews following the cursor, creating a directional distortion. |
-| SpinMouse | _(excluded from generated rules)_ Element rotates toward the cursor position. |
-| SwivelMouse | Element tilts in 3D around a chosen pivot axis following the cursor. |
-| Tilt3DMouse | Element tilts in 3D based on cursor position, rotating on X and Y axes from center. |
-| Track3DMouse | Element translates and tilts in 3D following the cursor, combining movement with perspective rotation. |
-| TrackMouse | Element follows the cursor with direct translation, no rotation. |
-
----
-
-## Key Constraints
-
-### Preset Categories
-
-These are categories of presets, each optimized for certain use cases but not limited to a single trigger mechanism.
-
-| Category | Optimized For | Implementation | Notes |
-| -------- | -------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------- |
-| entrance | When an element enters the viewport | `viewEnter` (intersection observer) | Can also be triggered by hover, click, animationend, and other triggers |
-| scroll | Scroll position of an element relative to document | ViewTimeline (scroll progress) | Animation progress tied to element's position in the viewport |
-| ongoing | Continuous loop | infinite CSS/WAAPI animation | Runs indefinitely until stopped |
-| mouse | Follow or Repel by Pointer position | transform values driven by pointer position | Real-time response to cursor position; may behave differently on mobile |
-
-### Trigger and Effect Binding
-
-In the simplest case, a trigger and its effect are bound to the same element. However, an effect on one element can also be triggered by another element (e.g., hovering a button triggers a FadeIn on a sibling panel).
-
-### Combining Effects
-
-1. Avoid mixing multiple effects on the same element at the same time when possible
-2. Never combine effects that affect the same CSS properties (e.g., two effects both using `transform`)
-3. When combining is necessary, effect order matters — later effects may override earlier ones
-4. If possible, use nested containers to separate effects that would conflict — place each effect on a separate wrapper element. Note: here also order matters
-
----
-
-## Parameter Standards
-
-### Animation Options (Not Preset Parameters)
-
-These are set on the effect configuration level, not on the preset itself:
-
-- `duration`: Animation duration in ms (entrance, ongoing)
-- `delay`: Animation delay in ms (entrance, ongoing)
-- `easing`: Easing function
-- `iterations`: Number of iterations
-- `alternate`: Alternate direction on each iteration
-- `fill`: Animation fill mode
-- `reversed`: Reverse the animation
-
-**Scroll-specific animation options:**
-
-- `rangeStart` / `rangeEnd`: `RangeOffset` controlling when the scroll animation starts/ends
-- `transitionDuration` / `transitionDelay` / `transitionEasing`: Transition smoothing
-
-### Preset-Specific Parameters
-
-**Most Scroll presets:**
-
-- `range`: 'in' | 'out' | 'continuous'
- - `'in'`: animation ends at the element's idle state (element animates in as it enters)
- - `'out'`: animation starts from the element's idle state (element animates out as it exits)
- - `'continuous'`: animation passes through the idle state (animates across the full scroll range)
-
-### Overloaded Parameter Names
-
-The `direction` parameter accepts different values depending on the preset:
-
-| Meaning | Accepted Values | Presets |
-| ------------------ | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
-| Cardinal | 'top', 'right', 'bottom', 'left' | FlipIn, FoldIn, SlideIn, FloatIn, RevealIn, ShuttersIn, Poke, Swing, Fold, RevealScroll, ShuttersScroll, SlideScroll |
-| Cardinal + center | 'top', 'right', 'bottom', 'left', 'center' | BounceIn |
-| Two sides | 'left', 'right' | TiltIn, PanScroll, SkewPanScroll, TiltScroll, TurnScroll |
-| Two sides + pseudo | 'left', 'right', 'pseudoLeft', 'pseudoRight' | CurveIn |
-| Corner | 'top-left', 'top-right', 'bottom-left', 'bottom-right' | TurnIn |
-| Eight directions | 4 cardinal + 4 diagonal | Cross |
-| Nine directions | 4 cardinal + 4 diagonal + 'center' | GrowScroll, ShrinkScroll |
-| Axis | 'horizontal', 'vertical' | WinkIn, ArcScroll, FlipScroll, Flip |
-| Axis + center | 'horizontal', 'vertical', 'center' | Breathe |
-| Rotation | 'clockwise', 'counter-clockwise' | SpinIn, SpinScroll, Spin |
-| Angle (number) | 0–360 (0° = right, 90° = top, 180° = left, 270° = bottom) | GlideIn, ExpandIn, MoveScroll |
-
-### Using Units
-
-Interact supports both a CSSUnitValue-style object (e.g., `distance: { value: 120, type: 'px' }`, mapped to the internal type `UnitLengthPercentage`) and flat string values (e.g., `distance: '120px'`).
-
-Prefer the object notation. In any case, be consistent within a configuration — use one format, not both.
-
-### Coordinate System
-
-**Standard:** 0° = right (east), angles increase counter-clockwise
-
-- 0° = right (east)
-- 90° = top (north)
-- 180° = left (west)
-- 270° = bottom (south)
-
-### Distance Units
-
-Supported unit types: `px`, `em`, `rem`, `vh`, `vw`, `vmin`, `vmax`, `percentage`
-
-```typescript
-distance: { value: 120, type: 'px' } // pixels
-distance: { value: 50, type: 'percentage' } // percentage
-distance: { value: 10, type: 'vh' } // viewport height
-```
-
-### CSS Custom Properties
-
-The library uses these CSS custom properties for runtime control:
-
-- `--motion-rotate`: Element rotation (used by SpinIn and other rotation presets)
-
----
-
-## Optional Parameters
-
-Some preset parameters are exposed, but their defaults have been tuned for good visual results and rarely need adjustment:
-
-### 3D Perspective
-
-| Preset | Parameter | Default | Range |
-| ----------------- | ------------- | ------- | -------- |
-| ArcIn | `perspective` | 800 | 200-2000 |
-| TiltIn | `perspective` | 800 | 200-2000 |
-| FoldIn | `perspective` | 800 | 200-2000 |
-| FlipIn | `perspective` | 800 | 200-2000 |
-| CurveIn | `perspective` | 200 | 100-1000 |
-| BounceIn (center) | `perspective` | 800 | 200-2000 |
-| ArcScroll | `perspective` | 500 | 200-2000 |
-| FlipScroll | `perspective` | 800 | 200-2000 |
-| TiltScroll | `perspective` | 400 | 200-2000 |
-| Spin3dScroll | `perspective` | 1000 | 200-2000 |
-
-### Depth (Z Translation)
-
-| Preset | Parameter | Default | Notes |
-| ------- | --------- | ------- | ---------------------- |
-| ArcIn | `depth` | 200px | Z translation distance |
-| CurveIn | `depth` | 300px | Z translation distance |
-| TiltIn | `depth` | 200px | Z translation distance |
-
----
-
-## Accessibility
-
-This section documents preset selection guidance for accessibility. It is not about library-level features (like `allowA11yTriggers`).
-
-### Host vs Preset Responsibility
-
-The presets generally provide animations; the host platform decides when/whether to apply them.
-
-Interact supports `conditions` in the config for handling reduced motion. Define a media condition for `(prefers-reduced-motion: reduce)` and use it to swap high-risk presets for safer alternatives (e.g., SpinIn → FadeIn, BounceIn → FadeIn). Conditions can be applied per-interaction or per-effect, and automatically re-evaluate when the user's preference changes.
-
-If it is known that the host handles accessibility globally (e.g., disabling all animations on `(prefers-reduced-motion: reduce)`), presets don't need to address it separately.
-
-### Preset Risk Levels
-
-_Note:_ this section should be confirmed by an a11y expert
-
-**High risk** (vestibular triggers, seizure risk if motion is fast and repetitive):
-
-- Spinning: SpinIn, Spin, SpinScroll, Spin3dScroll
-- Bouncing: BounceIn, Bounce
-- 3D rotations: ArcIn, FlipIn, ArcScroll, FlipScroll, Tilt3DMouse
-- Continuous motion: Flash, DVD, Jello, Wiggle
-
-**Medium risk** (strong motion, may affect some users):
-
-- TurnIn
-- ParallaxScroll at high speed values
-
-**Low risk / safe** (opacity/blur changes, minimal spatial movement):
-
-- FadeIn, FadeScroll, BlurIn, BlurScroll
-- SlideIn (subtle), GlideIn (subtle)
-- Pulse (subtle), Breathe
-
-### Reduced Motion Fallbacks
-
-| Original | Fallback |
-| --------------------------------- | ------------------------- |
-| BounceIn, SpinIn | FadeIn |
-| ArcIn, FlipIn, TurnIn | FadeIn |
-| Spin, Bounce, Wiggle | Stop or subtle Pulse |
-| Flash | Reduce frequency (<3/sec) |
-| ParallaxScroll | Static position |
-| ArcScroll, FlipScroll, SpinScroll | FadeScroll or disable |
-| All mouse presets | Static state |
-
-### LLM Guidance Principles
-
-1. **Do not limit creativity by default** — generate what the user asks for
-2. **Apply constraints only when explicitly requested** — keywords: "accessible", "a11y", "reduced motion safe", "subtle", "tone down"
-3. **High-risk presets are informational, not blockers** — optionally note vestibular concerns in response
-4. **Mouse presets may behave differently on mobile** — note this as context, not a restriction
-5. **Duration guidelines are suggestions** — functional UI <500ms, decorative up to 1200ms, hero up to 2000ms
-
----
-
-## Selection Tables
-
-### Selection by Atmosphere
-
-#### Playful / Fun / Whimsical
-
-Keywords: playful, fun, quirky, whimsical, lighthearted, bouncy, cheerful, cute, charming, goofy, jiggly, cheeky, springy, joyful, upbeat, poppy, friendly, casual, funky, groovy, surprising
-
-| Effect | Trigger | Preset |
-| ------ | -------- | ----------- |
-| Wink | entrance | WinkIn |
-| Wiggle | loop | Wiggle |
-| Jello | loop | Jello |
-| Poke | loop | Poke |
-| DVD | loop | DVD |
-| Cross | loop | Cross |
-| Spin | entrance | SpinIn |
-| Spin | scroll | SpinScroll |
-| Spin | loop | Spin |
-| Flip | entrance | FlipIn |
-| Flip | scroll | FlipScroll |
-| Flip | loop | Flip |
-| Bounce | entrance | BounceIn |
-| Bounce | loop | Bounce |
-| Swing | loop | Swing |
-| Blob | mouse | BlobMouse |
-| Rubber | loop | Rubber |
-| Track | mouse | TrackMouse |
-| Swivel | mouse | SwivelMouse |
-
-#### Smooth / Elegant / Refined
-
-Keywords: smooth, elegant, graceful, flowing, refined, sophisticated, polished, seamless, effortless, silky, controlled, classic, curved, rhythmic, continuous, circular, pendular, mesmerizing
-
-| Effect | Trigger | Preset |
-| ------ | -------- | ------------ |
-| Glide | entrance | GlideIn |
-| Swivel | mouse | SwivelMouse |
-| Turn | entrance | TurnIn |
-| Turn | scroll | TurnScroll |
-| Arc | entrance | ArcIn |
-| Arc | scroll | ArcScroll |
-| Slide | entrance | SlideIn |
-| Slide | scroll | SlideScroll |
-| Move | scroll | MoveScroll |
-| Fold | entrance | FoldIn |
-| Fold | loop | Fold |
-| Shape | entrance | ShapeIn |
-| Shape | scroll | ShapeScroll |
-| Fade | entrance | FadeIn |
-| Fade | scroll | FadeScroll |
-| Blur | entrance | BlurIn |
-| Blur | scroll | BlurScroll |
-| Blur | mouse | BlurMouse |
-| Float | entrance | FloatIn |
-| Airy | mouse | AiryMouse |
-| Pulse | loop | Pulse |
-| Swing | loop | Swing |
-| Shrink | entrance | DropIn |
-| Shrink | scroll | ShrinkScroll |
-
-#### Bold / Energetic / Dynamic
-
-Keywords: bold, dynamic, energetic, fast, impactful, attention-grabbing, eye-catching, striking, lively, electric, bright, sharp, snappy, quick, welcoming, opening, confident, blooming, emerging
-
-| Effect | Trigger | Preset |
-| -------- | -------- | -------------- |
-| 3D spin | scroll | Spin3dScroll |
-| Tilt | entrance | TiltIn |
-| Tilt | scroll | TiltScroll |
-| Resize | mouse | ScaleMouse |
-| Spin | entrance | SpinIn |
-| Spin | scroll | SpinScroll |
-| Spin | loop | Spin |
-| Flip | entrance | FlipIn |
-| Flip | scroll | FlipScroll |
-| Flip | loop | Flip |
-| Shutters | entrance | ShuttersIn |
-| Shutters | scroll | ShuttersScroll |
-| Bounce | entrance | BounceIn |
-| Bounce | loop | Bounce |
-| Grow | scroll | GrowScroll |
-| Flash | loop | Flash |
-| Expand | entrance | ExpandIn |
-| Stretch | scroll | StretchScroll |
-
-#### Soft / Gentle / Organic
-
-Keywords: soft, gentle, delicate, light, airy, breezy, wispy, floating, ethereal, dreamy, cloudy, hazy, atmospheric, gradual, subtle, calm, soothing, natural, zen, meditative, serene, relaxed, breathing, alive, organic
-
-| Effect | Trigger | Preset |
-| ------- | -------- | ------------ |
-| Breathe | loop | Breathe |
-| Float | entrance | FloatIn |
-| Airy | mouse | AiryMouse |
-| Blur | entrance | BlurIn |
-| Blur | scroll | BlurScroll |
-| Blur | mouse | BlurMouse |
-| Fade | entrance | FadeIn |
-| Fade | scroll | FadeScroll |
-| Pulse | loop | Pulse |
-| Shrink | entrance | DropIn |
-| Shrink | scroll | ShrinkScroll |
-| Expand | entrance | ExpandIn |
-
-#### Dramatic / Cinematic / Theatrical
-
-Keywords: dramatic, cinematic, theatrical, staged, sweeping, intimate, focused, detailed, revealing
-
-| Effect | Trigger | Preset |
-| -------- | -------- | -------------- |
-| Shutters | entrance | ShuttersIn |
-| Shutters | scroll | ShuttersScroll |
-| Parallax | scroll | ParallaxScroll |
-| Expand | entrance | ExpandIn |
-| Reveal | entrance | RevealIn |
-| Reveal | scroll | RevealScroll |
-
-#### Modern / Tech / Immersive
-
-Keywords: modern, tech, immersive, dimensional, spatial, 3d, depth, layered, innovative, interactive, responsive, engaging, following
-
-| Effect | Trigger | Preset |
-| -------- | -------- | -------------- |
-| Tilt 3D | mouse | Tilt3DMouse |
-| Track3D | mouse | Track3DMouse |
-| Track | mouse | TrackMouse |
-| Skew | mouse | SkewMouse |
-| 3D spin | scroll | Spin3dScroll |
-| Parallax | scroll | ParallaxScroll |
-| Resize | mouse | ScaleMouse |
-| Blur | entrance | BlurIn |
-| Blur | scroll | BlurScroll |
-| Blur | mouse | BlurMouse |
-| Fold | entrance | FoldIn |
-| Fold | loop | Fold |
-
-#### Creative / Experimental / Edgy
-
-Keywords: creative, artistic, experimental, unconventional, edgy, distorted, unique, expressive, graphic, transformative, fluid, liquid, elastic, flexible, stretchy
-
-| Effect | Trigger | Preset |
-| ------- | -------- | ------------- |
-| Skew | mouse | SkewMouse |
-| Tilt | entrance | TiltIn |
-| Tilt | scroll | TiltScroll |
-| Shape | entrance | ShapeIn |
-| Shape | scroll | ShapeScroll |
-| Blob | mouse | BlobMouse |
-| Cross | loop | Cross |
-| Stretch | scroll | StretchScroll |
-| Rubber | loop | Rubber |
-
-#### Clean / Professional / Minimal
-
-Keywords: clean, structured, organized, directional, purposeful, direct, simple, straightforward, progressive, minimalist, precise, understated, professional
-
-| Effect | Trigger | Preset |
-| ------ | -------- | ------------ |
-| Slide | entrance | SlideIn |
-| Slide | scroll | SlideScroll |
-| Move | scroll | MoveScroll |
-| Fold | entrance | FoldIn |
-| Fold | loop | Fold |
-| Reveal | entrance | RevealIn |
-| Reveal | scroll | RevealScroll |
-| Shrink | entrance | DropIn |
-| Shrink | scroll | ShrinkScroll |
-
-### Preset Selection Recommendations
-
-1. Do not add entrance presets (or any animation that starts with opacity 0) to `
` elements in the first fold
-2. Do not add scroll-in animations in the first fold
-3. Do not add scroll-out animations in the last fold
-
-### Cross-Category Parallels
-
-| Entrance | Scroll | Ongoing | Mouse |
-| ---------- | -------------- | ------- | ----------- |
-| FadeIn | FadeScroll | Flash | - |
-| ArcIn | ArcScroll | - | - |
-| SpinIn | SpinScroll | Spin | - |
-| BounceIn | - | Bounce | - |
-| TiltIn | TiltScroll | - | Tilt3DMouse |
-| FlipIn | FlipScroll | Flip | - |
-| FoldIn | - | Fold | - |
-| ExpandIn | GrowScroll | Pulse | ScaleMouse |
-| SlideIn | SlideScroll | - | TrackMouse |
-| BlurIn | BlurScroll | - | BlurMouse |
-| RevealIn | RevealScroll | - | - |
-| ShapeIn | ShapeScroll | - | - |
-| ShuttersIn | ShuttersScroll | - | - |
-| TurnIn | TurnScroll | - | - |
-| - | ParallaxScroll | - | TrackMouse |
-
----
-
-## Intensity Value Guide
-
-Tested values for different intensity levels of effects. When a user asks for "soft", "subtle", "medium", or "hard"/"dramatic" motion, use these as guidelines for suggesting appropriate parameter values.
-
-### Entrance Presets Intensity Values
-
-| Preset | Parameter | Subtle/Soft | Medium | Dramatic/Hard |
-| -------- | ---------------- | ----------- | ---------- | ------------- |
-| ArcIn | easing | sineOut | cubicInOut | quintInOut |
-| BlurIn | blur | 6px | 25px | 50px |
-| BounceIn | distanceFactor | 1 | 2 | 3 |
-| DropIn | initialScale | 1.2 | 1.6 | 2 |
-| FlipIn | initialRotate | 35° | 60° | 90° |
-| FoldIn | initialRotate | 35° | 60° | 90° |
-| ExpandIn | initialScale | 0.8 | 0.6 | 0 |
-| SlideIn | initialTranslate | 0.2 | 0.8 | 1 |
-| SpinIn | initialScale | 1 | 0.6 | 0 |
-
-### Scroll Presets Intensity Values
-
-| Preset | Parameter | Subtle/Soft | Medium | Dramatic/Hard |
-| ------------- | --------- | ----------- | ------ | ------------- |
-| BlurScroll | blur | 6px | 25px | 50px |
-| FlipScroll | rotate | 60° | 120° | 420° |
-| GrowScroll | scale | 1.2 | 1.7 | 4 |
-| MoveScroll | distance | 150px | 400px | 800px |
-| ShrinkScroll | scale | 0.8 | 0.3 | 0 |
-| SkewPanScroll | skew | 10° | 17° | 24° |
-| Spin3dScroll | rotate | 45° | 100° | 200° |
-| SpinScroll | scale | 1 | 0.7 | 0.4 |
-| StretchScroll | stretch | 1.2 | 1.5 | 2 |
-| TiltScroll | distance | 0 | 0.5 | 1 |
-| TurnScroll | scale | 1 | 1.3 | 1.6 |
-
-### Ongoing Presets Intensity Values
-
-| Preset | Parameter | Subtle/Soft | Medium | Dramatic/Hard |
-| ------ | --------- | ----------- | ------ | ------------- |
-| Bounce | intensity | 0 | 0.5 | 1 |
-| Fold | angle | 15° | 30° | 45° |
-| Jello | intensity | 0 | 0.33 | 1 |
-| Poke | intensity | 0 | 0.33 | 1 |
-| Pulse | intensity | 0 | 0.5 | 1 |
-| Rubber | intensity | 0 | 0.5 | 1 |
-| Swing | swing | 20° | 40° | 60° |
-| Wiggle | intensity | 0 | 0.33 | 1 |
-
-### Mouse Presets Intensity Values
-
-| Preset | Parameter(s) | Subtle/Soft | Medium | Dramatic/Hard |
-| ----------------- | ------------------ | ----------- | -------- | ------------- |
-| AiryMouse | angle | 10° | 50° | 85° |
-| BlobMouse | scale | 1.2 | 1.6 | 2.4 |
-| BlurMouse | angle, scale | 0°, 1 | 25°, 0.7 | 65°, 0.25 |
-| ScaleMouse (down) | scale | 0.85 | 0.5 | 0 |
-| ScaleMouse (up) | scale | 1.2 | 1.6 | 2.4 |
-| SkewMouse | angle | 10° | 20° | 45° |
-| SwivelMouse | angle, perspective | 25°, 1000 | 50°, 700 | 85°, 300 |
-| Tilt3DMouse | angle, perspective | 25°, 1000 | 50°, 500 | 85°, 200 |
-| Track3DMouse | angle, perspective | 25°, 1000 | 50°, 500 | 85°, 333 |
-
-### Intensity Usage Example
-
-When a user asks: "I want a subtle flip entrance"
-
-Suggest: `{ type: 'FlipIn', initialRotate: 35 }`
-
----
-
-## Preset Entry Format
-
-For each preset in the per-category reference files (`{category}-presets.md`):
-
-```markdown
-### PresetName
-
-Visual: [Use the description from the Preset Registry. Designer-approved descriptions must be used as-is.]
-
-Parameters:
-
-- `param1`: type/range (default: value)
-- `param2`: type/range (default: value)
-
-\`\`\`typescript
-{ type: 'PresetName', param1: 'value' }
-\`\`\`
-```
-
-**Notes:**
-
-- Include all required parameters
-- Include optional parameters with their defaults
-- For angle-based presets, note that 0° = right (east)
-- For 3D presets, include perspective parameter if customizable
-
----
-
-## Regeneration Steps
-
-To regenerate the preset reference files:
-
-### Step 1: Verify Registry
-
-Ensure the Preset Registry (above) is aligned with actual preset files in `packages/motion-presets/src/library/{category}/` (exclude index.ts and test files).
-
-### Step 2: Generate `presets-main.md`
-
-Build from these sections of this file:
-
-- **Terminology** → Terminology section
-- **Key Constraints** (categories table, trigger binding, combining effects) → Categories + Decision Flow + Combining Effects
-- **Parameter Standards** (all subsections) → Parameter Standards section
-- **Selection Tables** (by atmosphere, recommendations, cross-category parallels) → Selection Tables section
-- **Accessibility** (all subsections) → Accessibility section
-- **Preset Registry** → Available preset lists per category
-- Add progressive disclosure links to each `{category}-presets.md`
-- Keep under 500 lines
-
-### Step 3: Generate `{category}-presets.md` files
-
-For each category (entrance, scroll, ongoing, mouse):
-
-1. Read preset type definitions from `packages/motion-presets/src/types.ts`
-2. For each preset in that category, get params from `packages/motion-presets/src/library/{category}/{Preset}.ts`
-3. Write each preset entry using the **Preset Entry Format** above
-4. Append the **Optional Parameters** tables relevant to that category
-5. Append the **Intensity Value** table for that category from the Intensity Value Guide above
-6. For mouse: include mobile considerations note
-
-### Step 4: Validate
-
-1. `presets-main.md` is under 500 lines
-2. Heading hierarchy: `#` → `##` → `###` (no skipped levels)
-3. Every file has a table of contents after the title
-4. Every file has YAML frontmatter with `name` and `description` (see [Skills Compatibility](#skills-compatibility))
-5. Run `yarn format` on all generated markdown files to ensure they pass CI formatting checks
-6. Verify no content duplication between this plan and generated files (generated files should stand alone; this plan is the source, not a supplement)
diff --git a/.cursor/plans/sequence_docs_and_demos_9e654633.plan.md b/.cursor/plans/sequence_docs_and_demos_9e654633.plan.md
deleted file mode 100644
index 4575812c..00000000
--- a/.cursor/plans/sequence_docs_and_demos_9e654633.plan.md
+++ /dev/null
@@ -1,346 +0,0 @@
----
-name: Sequence docs and demos
-overview: Add documentation for the new Sequence/staggering feature to both the motion and interact docs, and create interactive demo components showcasing sequences with various triggers, easing functions, and configuration patterns.
-todos:
- - id: motion-api-sequence
- content: Create packages/motion/docs/api/sequence.md -- Sequence class API reference (constructor, addGroups, removeGroups, onFinish, offset calculation, inherited playback)
- status: completed
- - id: motion-api-get-sequence
- content: Create packages/motion/docs/api/get-sequence.md -- getSequence() and createAnimationGroups() function reference
- status: completed
- - id: motion-docs-updates
- content: 'Update motion docs: api/README.md index (add Sequence + getSequence entries), api/types.md (SequenceOptions, AnimationGroupArgs, IndexedGroup), core-concepts.md (Sequences & Staggering section)'
- status: completed
- - id: interact-guide-sequences
- content: Create packages/interact/docs/guides/sequences.md -- comprehensive sequences guide covering config, cross-element, listContainer, removal, conditions
- status: completed
- - id: interact-docs-updates
- content: 'Update interact docs: api/types.md (SequenceOptionsConfig, SequenceConfig, SequenceConfigRef, InteractConfig.sequences, Interaction.sequences, InteractCache.sequences), api/interact-class.md (getSequence, addToSequence, removeFromSequences, sequenceCache, elementSequenceMap), guides/README.md, examples/README.md, examples/list-patterns.md'
- status: completed
- - id: demo-sequence-playground
- content: Create SequencePlayground.tsx in both web/ and react/ -- interactive stagger controls
- status: completed
- - id: demo-sequence-entrance
- content: Create SequenceEntranceDemo.tsx in both web/ and react/ -- viewEnter staggered list
- status: completed
- - id: demo-sequence-click
- content: Create SequenceClickDemo.tsx in both web/ and react/ -- click-triggered multi-element sequence
- status: completed
- - id: demo-sequence-easing
- content: Create SequenceEasingComparison.tsx in both web/ and react/ -- side-by-side easing comparison
- status: completed
- - id: demo-app-integration
- content: Update App.tsx (web + react) and styles.css to include new sequence demos
- status: completed
-isProject: false
----
-
-# Sequence Feature Documentation and Demos
-
-## Part 1: Motion Package Docs (`packages/motion/docs/`)
-
-### 1.1 New file: `api/sequence.md`
-
-API reference for the `Sequence` class, mirroring the style of [animation-group.md](packages/motion/docs/api/animation-group.md). Contents:
-
-- **Overview** -- Sequence extends AnimationGroup to coordinate multiple AnimationGroups with staggered delays
-- **Class definition** -- constructor signature and properties:
-
-```typescript
-constructor(animationGroups: AnimationGroup[], options?: SequenceOptions)
-```
-
-| Property | Type | Default | Description |
-| ----------------- | ----------------------- | ------- | --------------------------------------------------- |
-| `animationGroups` | `AnimationGroup[]` | | Child groups managed by this Sequence |
-| `delay` | `number` | `0` | Base delay applied to all groups |
-| `offset` | `number` | `0` | Stagger offset (ms) between consecutive groups |
-| `offsetEasing` | `(p: number) => number` | linear | Easing function for stagger distribution |
-| `animations` | `Animation[]` | | Flattened array of all child animations (inherited) |
-| `ready` | `Promise` | | Resolves when all offsets have been applied |
-| `isCSS` | `boolean` | `false` | Whether animations use CSS mode (inherited) |
-
-- `**addGroups(entries: IndexedGroup[])**` -- inserts new groups at specified indices, recalculates offsets via `applyOffsets()`, and resets `ready`. Each `IndexedGroup` has `{ index: number, group: AnimationGroup }`.
-- `**removeGroups(predicate: (group: AnimationGroup) => boolean): AnimationGroup[]**` -- removes groups matching the predicate, cancels their animations, recalculates offsets for remaining groups, resets `ready`, and returns the removed groups. Used when list items are dynamically removed.
-- `**onFinish(callback: () => void): Promise**` -- overrides AnimationGroup's `onFinish` to await all child group `finished` promises before invoking the callback. Logs a warning for interrupted animations.
-- **Offset calculation** -- the formula `easing(i / last) * last * offset | 0` with examples for linear, quadIn, sineOut (from the spec). Single-group sequences always return `[0]`.
-- **Inherited playback API** from AnimationGroup: `play()`, `pause()`, `reverse()`, `cancel()`, `progress(p)`, `setPlaybackRate(rate)`, `getProgress()`, `getTimingOptions()`; getters: `playState`, `finished`
-- **Usage examples** -- creating a Sequence manually, controlling playback, using `addGroups`/`removeGroups`, using different easing functions
-
-### 1.2 New file: `api/get-sequence.md`
-
-API reference for the `getSequence()` and `createAnimationGroups()` functions (in `packages/motion/src/motion.ts`). Contents:
-
-- `**getSequence` signature:\*\*
-
-```typescript
-function getSequence(
- options: SequenceOptions,
- animationGroups: AnimationGroupArgs[],
- context?: Record,
-): Sequence;
-```
-
-Each `AnimationGroupArgs` entry is resolved into one or more `AnimationGroup` instances. If a target resolves to multiple elements (e.g. `HTMLElement[]` or a CSS selector string), each element becomes a separate group in the Sequence.
-
-- `**createAnimationGroups` signature:\*\*
-
-```typescript
-function createAnimationGroups(
- animationGroupArgs: AnimationGroupArgs[],
- context?: Record,
-): AnimationGroup[];
-```
-
-Builds `AnimationGroup[]` from args without wrapping in a Sequence. Used internally by `getSequence` and by `Interact.addToSequence()` when adding groups to an existing Sequence.
-
-- `**AnimationGroupArgs` type:\*\*
-
-```typescript
-type AnimationGroupArgs = {
- target: HTMLElement | HTMLElement[] | string | null;
- options: AnimationOptions;
- context?: Record;
-};
-```
-
-- **Examples** -- creating a staggered entrance for a list of elements, using different offset easings, building groups independently with `createAnimationGroups`
-
-### 1.3 Update `api/README.md`
-
-Add entries to the API index under "Core Functions":
-
-- `### [Sequence](sequence.md)` -- Coordinates multiple AnimationGroups with staggered delay offsets
-- `### [Sequence Creation](get-sequence.md)` -- `getSequence()` and `createAnimationGroups()` factory functions
-
-Add to "Quick Reference" section:
-
-```typescript
-// Sequence creation
-const sequence = getSequence(
- { offset: 200, offsetEasing: 'quadIn' },
- items.map((el) => ({ target: el, options: { name: 'FadeIn' } })),
-);
-sequence.play();
-```
-
-Add to "Types Overview": `SequenceOptions`, `AnimationGroupArgs`, `IndexedGroup`
-
-### 1.4 Update `api/types.md`
-
-Add new section `## Sequence Types` with:
-
-```typescript
-type SequenceOptions = {
- delay?: number;
- offset?: number;
- offsetEasing?: string | ((p: number) => number);
-};
-
-type AnimationGroupArgs = {
- target: HTMLElement | HTMLElement[] | string | null;
- options: AnimationOptions;
- context?: Record;
-};
-
-type IndexedGroup = {
- index: number;
- group: AnimationGroup;
-};
-```
-
-Include property descriptions and usage examples for each type.
-
-### 1.5 Update `core-concepts.md`
-
-Add a "Sequences & Staggering" section under "Advanced Concepts" explaining:
-
-- **Concept** -- Sequences coordinate multiple AnimationGroups as a single timeline with easing-driven stagger delays
-- **Offset model** -- how `offset` distributes delay across groups using the formula `easing(i / last) * last * offset | 0`
-- **Easing curves** -- visual explanation of how `linear`, `quadIn`, `sineOut`, and custom `cubic-bezier` affect stagger timing (quadIn = slow start then rapid, sineOut = fast start then gradual)
-- **Dynamic groups** -- `addGroups` for adding elements (e.g. new list items) and `removeGroups` for cleanup when elements are removed, both triggering automatic offset recalculation
-- **Relationship to AnimationGroup** -- Sequence inherits all playback controls; child groups are stored in `animationGroups` while `animations` contains the flattened array
-
----
-
-## Part 2: Interact Package Docs (`packages/interact/docs/`)
-
-### 2.1 New file: `guides/sequences.md`
-
-Comprehensive guide for using sequences in Interact configs. Contents:
-
-- **What is a Sequence** -- a list of Effects managed as a coordinated timeline with staggered delays, built on top of the Motion `Sequence` class
-- **Config structure** -- two levels of sequence definition:
- - `InteractConfig.sequences` -- reusable named sequences (keyed map, resolved by `sequenceId`)
- - `Interaction.sequences` -- per-interaction sequence list (inline `SequenceConfig` or `SequenceConfigRef` references)
- - An interaction can have both `effects` and `sequences`, or either alone
-- **SequenceConfig** -- inline sequence definition:
-
-```typescript
-type SequenceConfig = SequenceOptionsConfig & {
- effects: (Effect | EffectRef)[];
-};
-```
-
-- **SequenceConfigRef** -- referencing a reusable sequence by ID with optional inline overrides:
-
-```typescript
-type SequenceConfigRef = {
- sequenceId: string;
- delay?: number;
- offset?: number;
- offsetEasing?: string | ((p: number) => number);
- conditions?: string[];
-};
-```
-
-- **SequenceOptionsConfig** -- shared options (includes `conditions` for media-query gating):
-
-```typescript
-type SequenceOptionsConfig = {
- delay?: number;
- offset?: number;
- offsetEasing?: string | ((p: number) => number);
- sequenceId?: string;
- conditions?: string[];
-};
-```
-
-- **Offset and easing** -- how offset distributes delay across effects, easing curves (linear, quadIn, sineOut), visual formula `easing(i / last) * last * offset | 0`
-- **Cross-element sequences** -- effects targeting different `key` values within a single sequence, resolved at add-time via `_processSequencesForTarget`. When a sequence effect targets a different key than the source interaction, Interact waits for both elements to be registered before creating the Sequence.
-- **Sequences with listContainer** -- staggering list items:
- - Initial `add()` creates the Sequence with all existing list items
- - `addListItems()` calls `Interact.addToSequence()` with `IndexedGroup` entries at the correct indices, triggering offset recalculation
- - `removeListItems()` calls `Interact.removeFromSequences()` which uses the `elementSequenceMap` WeakMap for O(1) lookup and calls `sequence.removeGroups()` with a predicate matching the removed element's animations
- - Each `addListItems` call uses a unique cache key (`${cacheKey}::${generateId()}`) for its Sequence
-- **Element removal and cleanup** -- how `Interact.removeFromSequences(elements)` uses `elementSequenceMap` (a `WeakMap>`) for efficient element-to-sequence lookup, calls `removeGroups` on each associated Sequence, and deletes the element from the map. Called automatically from `removeListItems`.
-- **Conditions on sequences** -- sequence-level `conditions` array gates the entire sequence; individual effect-level conditions within `effects` can gate specific effects. Both set up `matchMedia` listeners for dynamic add/remove.
-- **Sequence caching** -- `Interact.sequenceCache` (`Map`) prevents duplicate Sequences for the same interaction/key combination. `Interact.destroy()` and `clearInteractionStateForKey()` clean up cache entries.
-- **Examples** -- staggered card grid entrance (viewEnter + listContainer), multi-element orchestration (cross-key sequence), click-triggered alternate sequence, sequence with media-query conditions
-
-### 2.2 Update `api/types.md`
-
-Add new section `## Sequence Types` with type definitions:
-
-- `SequenceOptionsConfig` -- with all properties including `conditions?: string[]`
-- `SequenceConfig` -- `SequenceOptionsConfig & { effects: (Effect | EffectRef)[] }`
-- `SequenceConfigRef` -- reference type with `sequenceId` and optional overrides + `conditions`
-- Updated `InteractConfig` showing `sequences?: Record`
-- Updated `Interaction` showing `sequences?: (SequenceConfig | SequenceConfigRef)[]` with note on mutual exclusivity branches (effects-only, sequences-only, or both)
-- Updated `InteractCache` showing `sequences: { [sequenceId: string]: SequenceConfig }` and `interactions[path].sequences: Record`
-
-### 2.3 Update `api/interact-class.md`
-
-Add new static methods and properties under "Static Methods":
-
-- `**Interact.getSequence(cacheKey, sequenceOptions, animationGroupArgs, context?)`\*\*
- - Parameters: `cacheKey: string`, `sequenceOptions: SequenceOptions`, `animationGroupArgs: AnimationGroupArgs[]`, `context?: { reducedMotion?: boolean }`
- - Returns: `Sequence`
- - Details: Returns cached Sequence if one exists for `cacheKey`, otherwise creates via `getSequence()` from `@wix/motion`, caches it, and registers target elements in `elementSequenceMap`
-- `**Interact.addToSequence(cacheKey, animationGroupArgs, indices, context?)**`
- - Parameters: `cacheKey: string`, `animationGroupArgs: AnimationGroupArgs[]`, `indices: number[]`, `context?: { reducedMotion?: boolean }`
- - Returns: `boolean` (false if no cached Sequence found for `cacheKey`)
- - Details: Builds new `AnimationGroup` instances via `createAnimationGroups()`, maps them to `IndexedGroup[]` using `indices`, calls `cached.addGroups(entries)`, and registers new elements in `elementSequenceMap`
-- `**Interact.removeFromSequences(elements)**`
- - Parameters: `elements: HTMLElement[]`
- - Returns: `void`
- - Details: For each element, looks up associated Sequences via `elementSequenceMap`, calls `sequence.removeGroups()` with a predicate matching animations targeting that element, and deletes the element from the map
-- `**Interact.sequenceCache**` -- `Map` static property, cleared on `destroy()`
-- `**Interact.elementSequenceMap**` -- `WeakMap>` static property, reset on `destroy()`. Provides O(1) element-to-Sequence lookup for efficient removal.
-
-### 2.4 Update `guides/README.md`
-
-Add entry under guide list:
-
-- `### 🎼 Sequences & Staggering` -- Coordinate multiple effects with staggered timing, offset easing, and dynamic list management. Link to `guides/sequences.md`.
-
-### 2.5 Update `examples/README.md` and `examples/list-patterns.md`
-
-`**examples/README.md`:\*\*
-
-- Add "Sequence Animations" category under "Example Categories" with sub-items: Staggered List Entrance, Cross-Element Orchestration, Click-Triggered Sequence, Easing Comparison
-- Update "Advanced Patterns > Animation Sequences" to reference the new `sequences` config syntax as the preferred approach
-
-`**examples/list-patterns.md`:\*\*
-
-- Add new section `## Sequence-Based Staggering` with examples showing:
- - Staggered list entrance using `Interaction.sequences` with `listContainer`
- - Dynamic list items with `addListItems` triggering `addToSequence`
- - Different `offsetEasing` values (linear vs quadIn vs sineOut) for list stagger
- - Sequence with removal: how removing list items automatically cleans up via `removeFromSequences`
-
----
-
-## Part 3: Demo App (`apps/demo/`)
-
-Create demo components in both `src/web/components/` and `src/react/components/` (following the existing mirror pattern). Each demo uses the `useInteractInstance` hook and the existing panel/control UI patterns.
-
-### 3.1 `SequencePlayground.tsx` -- Interactive Stagger Controls
-
-An interactive demo (like the existing `Playground.tsx`) where the user can tune sequence parameters in real time:
-
-- **Controls**: offset (0-500ms slider), offsetEasing (dropdown: linear, quadIn, quadOut, sineOut, cubic-bezier), delay (0-500ms), duration per effect, trigger type (viewEnter, click)
-- **Preview**: a grid of 6-8 cards, each as an effect in a sequence, using `keyframeEffect` (e.g. fade+slide-up)
-- **Config display**: shows the live `InteractConfig` JSON being used
-- Uses `Interaction.sequences` with inline sequence definition
-
-### 3.2 `SequenceEntranceDemo.tsx` -- ViewEnter Staggered List
-
-A scroll-triggered staggered entrance showcasing the most common use case:
-
-- A list of cards inside a `listContainer`, entering the viewport with staggered `viewEnter` trigger
-- Demonstrates `offset` + `offsetEasing: 'quadIn'` for natural-feeling stagger
-- Showcases both inline and reusable (`sequenceId`) sequence definitions
-
-### 3.3 `SequenceClickDemo.tsx` -- Click-Triggered Sequence
-
-A click-triggered multi-element orchestration:
-
-- A button triggers a sequence that animates multiple elements (heading, body text, image) in coordinated order
-- Demonstrates cross-element targeting (effects with different `key` values in the sequence)
-- Uses `click` trigger with `type: 'alternate'` for play/reverse
-
-### 3.4 `SequenceEasingComparison.tsx` -- Side-by-Side Easing Curves
-
-A visual comparison of different `offsetEasing` values:
-
-- 3-4 rows, each showing the same set of items but with different easing (linear, quadIn, sineOut, cubicBezier)
-- All triggered simultaneously on a button click or viewEnter
-- Labels showing easing name and computed delay values
-
-### 3.5 Update `App.tsx` (both web and react)
-
-Add the new demo components to both App files, with appropriate section titles. Add a "Sequences" section header separating existing demos from the new sequence demos.
-
-### 3.6 Update `src/styles.css`
-
-Add styles for the new sequence demo components (card grids, easing comparison rows, sequence preview areas). Follow the existing design system (Space Grotesk/Inter fonts, dark panels, blue accent).
-
----
-
-## File Summary
-
-| Action | Path |
-| ------ | ------------------------------------------------------------- |
-| Create | `packages/motion/docs/api/sequence.md` |
-| Create | `packages/motion/docs/api/get-sequence.md` |
-| Edit | `packages/motion/docs/api/README.md` |
-| Edit | `packages/motion/docs/api/types.md` |
-| Edit | `packages/motion/docs/core-concepts.md` |
-| Create | `packages/interact/docs/guides/sequences.md` |
-| Edit | `packages/interact/docs/api/types.md` |
-| Edit | `packages/interact/docs/api/interact-class.md` |
-| Edit | `packages/interact/docs/guides/README.md` |
-| Edit | `packages/interact/docs/examples/README.md` |
-| Edit | `packages/interact/docs/examples/list-patterns.md` |
-| Create | `apps/demo/src/web/components/SequencePlayground.tsx` |
-| Create | `apps/demo/src/web/components/SequenceEntranceDemo.tsx` |
-| Create | `apps/demo/src/web/components/SequenceClickDemo.tsx` |
-| Create | `apps/demo/src/web/components/SequenceEasingComparison.tsx` |
-| Create | `apps/demo/src/react/components/SequencePlayground.tsx` |
-| Create | `apps/demo/src/react/components/SequenceEntranceDemo.tsx` |
-| Create | `apps/demo/src/react/components/SequenceClickDemo.tsx` |
-| Create | `apps/demo/src/react/components/SequenceEasingComparison.tsx` |
-| Edit | `apps/demo/src/web/App.tsx` |
-| Edit | `apps/demo/src/react/App.tsx` |
-| Edit | `apps/demo/src/styles.css` |
diff --git a/.cursor/plans/sequence_feature_implementation_84c01c97.plan.md b/.cursor/plans/sequence_feature_implementation_84c01c97.plan.md
deleted file mode 100644
index 6bf4593f..00000000
--- a/.cursor/plans/sequence_feature_implementation_84c01c97.plan.md
+++ /dev/null
@@ -1,317 +0,0 @@
----
-name: Sequence implementation
-overview: implement a new Sequence class that allows controling playback of mutiple AnimationGroups, and integrate it with Motion and Interact libraries.
-todos:
- - id: motion-sequence-class
- content: Create Sequence class in packages/motion/src/Sequence.ts
- status: completed
- - id: motion-sequence-types
- content: Add SequenceOptions type to packages/motion/src/types.ts
- status: completed
- dependencies:
- - motion-sequence-class
- - id: motion-sequence-export
- content: Export Sequence and SequenceOptions from packages/motion/src/index.ts
- status: completed
- dependencies:
- - motion-sequence-class
- - motion-sequence-types
- - id: motion-get-sequence
- content: Implement getSequence() function in packages/motion/src/motion.ts and export it
- status: completed
- dependencies:
- - motion-sequence-class
- - motion-sequence-types
- - id: interact-types
- content: Update types in packages/interact/src/types.ts (SequenceOptionsConfig, SequenceConfig, SequenceConfigRef, InteractConfig, Interaction)
- status: completed
- dependencies:
- - motion-sequence-types
- - id: interact-cache-types
- content: Update InteractCache type to include sequences field
- status: completed
- dependencies:
- - interact-types
- - id: interact-parse-config
- content: Update parseConfig in packages/interact/src/core/Interact.ts to handle sequences
- status: completed
- dependencies:
- - interact-types
- - interact-cache-types
- - id: interact-add
- content: Update effect processing in packages/interact/src/core/add.ts to create Sequence instances
- status: completed
- dependencies:
- - motion-get-sequence
- - interact-parse-config
- - id: interact-sequence-cache
- content: Implement Sequence caching on Interact class (sequenceCache static property and getEffect() endpoint)
- status: completed
- dependencies:
- - interact-add
- - id: interact-handlers
- content: Update trigger handlers (viewEnter.ts, click.ts, etc.) to support Sequence instances
- status: completed
- dependencies:
- - interact-add
- - id: tests-unit
- content: Write unit tests for Sequence class offset calculations and easing integration
- status: completed
- dependencies:
- - motion-sequence-class
- - id: tests-integration
- content: Write integration tests for sequence parsing in Interact
- status: completed
- dependencies:
- - interact-parse-config
- - id: tests-e2e
- content: Write E2E tests for staggered animations with various easing functions
- status: pending
- dependencies:
- - interact-handlers
-isProject: false
----
-
-# Sequence Feature Implementation
-
-This plan implements the Sequence feature as specified in [sequences-spec.md](packages/interact/dev/sequences-spec.md). The feature enables managing multiple Effects as a coordinated timeline with staggered delays.
-
-## Architecture Overview
-
-```mermaid
-classDiagram
- class AnimationGroup {
- +animations: Animation[]
- +options: AnimationGroupOptions
- +ready: Promise
- +play()
- +pause()
- +reverse()
- +cancel()
- +onFinish()
- }
-
- class Sequence {
- +animationGroups: AnimationGroup[]
- +delay: number
- +offset: number
- +offsetEasing: function
- +play()
- +pause()
- +reverse()
- +cancel()
- +onFinish()
- -calculateOffsets()
- }
-
- Sequence --|> AnimationGroup : extends
- Sequence "1" --> "*" AnimationGroup : manages
-```
-
-## Part 1: @wix/motion Package Changes
-
-### 1.1 Create Sequence Class
-
-Create new file `packages/motion/src/Sequence.ts`:
-
-- Extend `AnimationGroup` to inherit the playback control API
-- Store `animations: AnimationGroup[]` instead of `animations: Animation[]`
-- Add properties: `delay`, `offset`, `offsetEasing`
-- Implement `calculateOffsets()` method using the formula from spec:
-
-```typescript
-const last = indices.at(-1);
-indices.map((n) => (easing(n / last) * last * offset) | 0);
-```
-
-- Override playback methods/properties where needed to delegate to child `AnimationGroup` instances
-- Apply calculated delay offsets to each effect's animation timing
-
-### 1.2 Add Sequence Types
-
-Update `packages/motion/src/types.ts`:
-
-```typescript
-export type SequenceOptions = {
- delay?: number; // default 0
- offset?: number; // default 0
- offsetEasing?: string | ((p: number) => number);
-};
-```
-
-### 1.3 Export Sequence
-
-Update `packages/motion/src/index.ts` to export:
-
-- `Sequence` class
-- `SequenceOptions` type
-
-### 1.4 Implement a `getSequence()`
-
-- Create this function in `packages/motion/src/motion.ts`
-- Export it via `packages/motion/src/index.ts`
-- It should have the following signature:
-
-```ts
-type AnimationGroupArgs = {
- target: HTMLElement | HTMLElement[] | string | null;
- options: AnimationOptions;
- context?: Record;
-};
-
-type getSequence = (
- options: SequenceOptions,
- animations: AnimationGroupArgs | AnimationGroupArgs[],
-) => Sequence;
-```
-
-The `getSequence()` funciton is passed `animations: AnimationGroupArgs[]` it creates a `Sequence` from a each effect definition in the array.
-If an `Effect` in the array resolves to multiple elements, each resulting instance becomes an effect in the array.
-
-## Part 2: @wix/interact Package Changes
-
-### 2.1 Update Types
-
-Update `packages/interact/src/types.ts`:
-
-```typescript
-// New SequenceOptions type
-export type SequenceOptionsConfig = {
- delay?: number; // default 0
- offset?: number; // default 0
- offsetEasing?: string | ((p: number) => number); // default linear
- sequenceId?: string; // for referencing a reusable sequence declaration
-};
-
-// New SequenceConfig type
-export type SequenceConfig = SequenceOptionsConfig & {
- effects: (Effect | EffectRef)[];
-};
-
-// New SequenceConfigRef type
-export type SequenceConfigRef = {
- sequenceId: string;
-} & {
- delay?: number; // default 0
- offset?: number; // default 0
- offsetEasing?: string | ((p: number) => number); // default linear
-};
-
-// Update InteractConfig
-export type InteractConfig = {
- effects: Record;
- sequences?: Record; // NEW: reusable sequences
- conditions?: Record;
- interactions: Interaction[];
-};
-
-// Update Interaction - use mutually exclusive branches for proper type narrowing
-export type Interaction = InteractionTrigger &
- (
- | {
- effects: ((Effect | EffectRef) & { interactionId?: string })[];
- sequences?: never; // effects-only: explicitly exclude sequences
- }
- | {
- effects?: never; // sequences-only: explicitly exclude effects
- sequences: (SequenceConfig | SequenceConfigRef)[];
- }
- | {
- effects: ((Effect | EffectRef) & { interactionId?: string })[];
- sequences: (SequenceConfig | SequenceConfigRef)[];
- }
- );
-```
-
-### 2.2 Update InteractCache
-
-Add sequences to the cache structure in `packages/interact/src/types.ts`:
-
-```typescript
-export type InteractCache = {
- effects: { [effectId: string]: Effect };
- sequences: { [sequenceId: string]: SequenceConfig }; // NEW
- conditions: { [conditionId: string]: Condition };
- interactions: {
- [path: string]: {
- triggers: Interaction[];
- effects: Record;
- sequences: Record;
- interactionIds: Set;
- selectors: Set;
- };
- };
-};
-```
-
-### 2.3 Update parseConfig Function
-
-Modify `packages/interact/src/core/Interact.ts`:
-
-1. Parse `config.sequences` into cache (similar to `config.effects`)
-2. Process `interaction.sequences` array:
-
-- Resolve `sequenceId` references from `config.sequences`
-- Process each effect within the sequence
-- Generate unique IDs for sequence effects
-
-1. Track sequence membership for effects (needed for delay calculation)
-
-### 2.4 Update Effect Processing in `add.ts`
-
-Modify `packages/interact/src/core/add.ts`:
-
-1. When adding interactions, check if effects belong to a sequence
-2. Create `Sequence` instance from `@wix/motion` for grouped effects
-3. Apply calculated delay offsets based on effect index in sequence
-4. Handle sequence removal (when conditions change or elements removed)
-
-### 2.5 Create `Sequence` caching:
-
-- Cache created `Sequence` instances on a static property `Interact.sequenceCache`
-- Add endpoint on `Interact` class to get cached `Sequence` instances
-- Add a new endpoint on `Interact.getEffect()` class that wraps `getAnimation()` and `getSequence()` of `@wix/motion` and, depending on the provided arguments, either:
- - Returns a cached `Sequence` if there's one, or
- - Creates a new `Sequence`, or
- - Returns an `AnimationGroup`
-
-### 2.6 Handler Integration
-
-Update relevant trigger handlers (e.g., `viewEnter.ts`, `click.ts`) to:
-
-- Accept `Sequence` instances in addition to individual `AnimationGroup`
-- Properly manage sequence playback (play, pause, cancel)
-
-## Part 3: Offset Calculation Implementation
-
-The offset calculation follows this algorithm:
-
-```typescript
-function calculateOffsets(
- count: number,
- offset: number,
- easingFn: (t: number) => number,
-): number[] {
- if (count <= 1) return [0];
-
- const last = count - 1;
- return Array.from({ length: count }, (_, i) => (easingFn(i / last) * last * offset) | 0);
-}
-```
-
-The calculated offsets are added to each effect's existing `delay` property.
-
-## Key Implementation Notes
-
-1. **Initial Scope**: Only `keyframeEffect` and `namedEffect` types (not `customEffect`)
-2. **Skip `align` Property**: Per spec, do not implement the `align` property yet
-3. **Effect Removal**: When an effect is removed (e.g., condition no longer matches), recalculate delays for remaining effects
-4. **Sequence Removal**: Optimize to avoid recalculating when entire sequence is removed
-5. **No Element Target**: `Sequence` has no `key` property - targeting is per-effect
-
-## Testing Strategy
-
-1. Unit tests for `Sequence` class offset calculations
-2. Unit tests for easing function integration
-3. Integration tests for sequence parsing in Interact
-4. E2E tests for staggered animations with various easing functions
diff --git a/.cursor/plans/sequence_removegroups_support_9b4d0693.plan.md b/.cursor/plans/sequence_removegroups_support_9b4d0693.plan.md
deleted file mode 100644
index e4a81dde..00000000
--- a/.cursor/plans/sequence_removegroups_support_9b4d0693.plan.md
+++ /dev/null
@@ -1,266 +0,0 @@
----
-name: Sequence removeGroups support
-overview: Add a `removeGroups` method to the Sequence class and an element-to-Sequence WeakMap cache in Interact, then wire removal into `removeListItems` so that removing DOM elements efficiently removes their corresponding AnimationGroups from cached Sequences.
-todos:
- - id: tests-motion-removeGroups
- content: Write failing tests for `Sequence.removeGroups()` in `packages/motion/test/Sequence.spec.ts`
- status: completed
- - id: tests-interact-removeFromSequence
- content: Write failing tests for `removeListItems` sequence cleanup and elementSequenceMap in `packages/interact/test/sequences.spec.ts`
- status: completed
- - id: impl-motion-removeGroups
- content: Implement `Sequence.removeGroups(predicate)` in `packages/motion/src/Sequence.ts`
- status: completed
- - id: impl-interact-elementSequenceMap
- content: Add `Interact.elementSequenceMap` WeakMap, populate it in `getSequence`/`addToSequence`, add `removeFromSequences`
- status: completed
- - id: impl-interact-wire-removal
- content: Call `Interact.removeFromSequences(elements)` from `removeListItems` and clean map in `clearInteractionStateForKey`
- status: completed
- - id: verify-tests-pass
- content: Run all tests to verify new tests pass and no regressions
- status: completed
-isProject: false
----
-
-# Sequence `removeGroups` Implementation Plan
-
-## Problem
-
-When list items are removed from the DOM (via `removeListItems` or the MutationObserver in `_childListChangeHandler`), the trigger handler cleanup runs (`module.remove(element)`) but the cached `Sequence` objects still hold `AnimationGroup` instances targeting the removed elements. This means:
-
-- Stagger offset calculations remain based on stale groups
-- Playback iterates over dead animations (targeting detached elements)
-- Memory leaks from retaining references to removed DOM nodes
-
-## Design
-
-### Layer 1: Motion package -- `Sequence.removeGroups()`
-
-Add a `removeGroups` method to the `Sequence` class (mirror of `addGroups`) that:
-
-1. Accepts a predicate function to identify groups to remove
-2. Removes matching groups from `this.animationGroups`, `this.timingOptions`, and `this.animations`
-3. Cancels removed animations before removal
-4. Recalculates offsets via `this.applyOffsets()` for remaining groups
-5. Resets `this.ready` promise
-6. Returns the removed groups (useful for testing and for the caller to do further cleanup)
-
-API:
-
-```typescript
-removeGroups(predicate: (group: AnimationGroup) => boolean): AnimationGroup[]
-```
-
-This allows the Interact layer to match groups by reference (looked up from the WeakMap cache).
-
-### Layer 2: Interact package -- `elementSequenceMap` WeakMap + `removeFromSequences`
-
-#### The Problem with Brute-Force Iteration
-
-A naive approach would iterate `sequenceCache.values()` then each sequence's `animationGroups` then each group's `animations` to find the target element. This is O(sequences x groups x animations) on every removal -- too expensive.
-
-#### Solution: `elementSequenceMap` WeakMap
-
-Add a `WeakMap>` on the `Interact` class that provides O(1) lookup from a target element to the set of Sequences containing it.
-
-```typescript
-static elementSequenceMap = new WeakMap>();
-```
-
-**Why `WeakMap`**: Keys are `HTMLElement` references. When an element is removed from the DOM and all JS references to it are released, the WeakMap entry is automatically garbage-collected. No manual cleanup needed for GC purposes.
-
-**Why `Set`**: A single element could theoretically appear in multiple Sequences (e.g. if the same element participates in different interaction sequences). Using a Set avoids duplicates and allows efficient deletion.
-
-#### Population: When Sequences are Created or Extended
-
-Both `Interact.getSequence()` and `Interact.addToSequence()` call into `@wix/motion` to create `AnimationGroup` instances. After creation, these methods already know the resulting `Sequence` and can derive the target elements from `animationGroupArgs[].target`. We populate the map at these two sites:
-
-In **`Interact.getSequence()`** -- after creating the Sequence, register all target elements:
-
-```typescript
-static getSequence(cacheKey, sequenceOptions, animationGroupArgs, context): Sequence {
- const cached = Interact.sequenceCache.get(cacheKey);
- if (cached) return cached;
-
- const sequence = getMotionSequence(sequenceOptions, animationGroupArgs, context);
- Interact.sequenceCache.set(cacheKey, sequence);
-
- // Populate element -> Sequence lookup
- Interact._registerSequenceElements(animationGroupArgs, sequence);
-
- return sequence;
-}
-```
-
-In **`Interact.addToSequence()`** -- after adding groups to an existing Sequence:
-
-```typescript
-static addToSequence(cacheKey, animationGroupArgs, indices, context): boolean {
- const cached = Interact.sequenceCache.get(cacheKey);
- if (!cached) return false;
-
- const newGroups = createAnimationGroups(animationGroupArgs, context);
- // ... existing addGroups logic ...
- cached.addGroups(entries);
-
- // Populate element -> Sequence lookup for new elements
- Interact._registerSequenceElements(animationGroupArgs, cached);
-
- return true;
-}
-```
-
-The shared helper resolves elements from `AnimationGroupArgs.target`:
-
-```typescript
-private static _registerSequenceElements(
- animationGroupArgs: AnimationGroupArgs[],
- sequence: Sequence,
-): void {
- for (const { target } of animationGroupArgs) {
- const elements = Array.isArray(target) ? target
- : target instanceof HTMLElement ? [target]
- : [];
- for (const el of elements) {
- let seqs = Interact.elementSequenceMap.get(el);
- if (!seqs) {
- seqs = new Set();
- Interact.elementSequenceMap.set(el, seqs);
- }
- seqs.add(sequence);
- }
- }
-}
-```
-
-#### Removal: `Interact.removeFromSequences(elements)`
-
-When elements are removed, the lookup is O(elements) instead of O(sequences x groups x animations):
-
-```typescript
-static removeFromSequences(elements: HTMLElement[]): void {
- for (const element of elements) {
- const sequences = Interact.elementSequenceMap.get(element);
- if (!sequences) continue;
-
- for (const sequence of sequences) {
- sequence.removeGroups((group) =>
- group.animations.some(
- (a) => (a.effect as KeyframeEffect)?.target === element,
- ),
- );
- }
-
- Interact.elementSequenceMap.delete(element);
- }
-}
-```
-
-This is called from `removeListItems` in [packages/interact/src/core/remove.ts](packages/interact/src/core/remove.ts):
-
-```typescript
-export function removeListItems(elements: HTMLElement[]) {
- const modules = Object.values(TRIGGER_TO_HANDLER_MODULE_MAP);
- for (const element of elements) {
- for (const module of modules) {
- module.remove(element);
- }
- }
- Interact.removeFromSequences(elements);
-}
-```
-
-#### Cleanup on `Interact.destroy()`
-
-`Interact.destroy()` already clears `sequenceCache`. Since `elementSequenceMap` is a `WeakMap`, it does not need explicit clearing (its entries are GC'd when elements are collected). However, for consistency and to avoid stale `Set` references during the same session, we replace it:
-
-```typescript
-static destroy(): void {
- // ... existing cleanup ...
- Interact.sequenceCache.clear();
- Interact.elementSequenceMap = new WeakMap();
-}
-```
-
-## File Changes
-
-### [packages/motion/src/Sequence.ts](packages/motion/src/Sequence.ts)
-
-Add `removeGroups(predicate)` method:
-
-- Iterate `animationGroups` and partition into keep/remove based on predicate
-- Cancel animations in removed groups
-- Rebuild `animationGroups`, `timingOptions`, and `animations` arrays (keeping order)
-- Call `applyOffsets()` and reset `ready`
-- Return removed groups array
-
-### [packages/interact/src/core/Interact.ts](packages/interact/src/core/Interact.ts)
-
-- Add `static elementSequenceMap = new WeakMap>()`
-- Add `private static _registerSequenceElements(args, sequence)` helper
-- Modify `static getSequence()` -- call `_registerSequenceElements` after creating the Sequence
-- Modify `static addToSequence()` -- call `_registerSequenceElements` after adding groups
-- Add `static removeFromSequences(elements: HTMLElement[])` -- look up and remove via WeakMap
-- Modify `static destroy()` -- reset `elementSequenceMap`
-
-### [packages/interact/src/core/remove.ts](packages/interact/src/core/remove.ts)
-
-- Call `Interact.removeFromSequences(elements)` at the end of `removeListItems`
-
-## Test-First Approach
-
-### Motion tests -- [packages/motion/test/Sequence.spec.ts](packages/motion/test/Sequence.spec.ts)
-
-New `describe('removeGroups')` section:
-
-- **removes groups matching predicate** -- verify `animationGroups` array shrinks
-- **removes corresponding entries from animations array** -- verify flattened `animations` updated
-- **removes corresponding entries from timingOptions** -- verify via subsequent `addGroups` still working correctly
-- **cancels animations in removed groups** -- verify `cancel()` called on removed group's animations
-- **recalculates offsets after removal** -- verify delays/endDelays recomputed for remaining groups
-- **updates ready promise after removal** -- verify new `ready` resolves
-- **returns removed groups** -- verify return value contains the removed AnimationGroup instances
-- **no-op when predicate matches nothing** -- verify arrays unchanged
-- **handles removing all groups (empty sequence)** -- verify graceful empty state
-- **handles removing from single-group sequence** -- verify offset edge case (single -> empty)
-
-### Interact tests -- [packages/interact/test/sequences.spec.ts](packages/interact/test/sequences.spec.ts)
-
-New suite (Suite H or extend Suite D/E):
-
-- **elementSequenceMap is populated when Sequence is created via getSequence** -- verify WeakMap has entries for target elements
-- **elementSequenceMap is populated when groups are added via addToSequence** -- verify new elements are registered
-- **removeFromSequences calls removeGroups on the correct Sequence** -- verify mock `removeGroups` called
-- **removeFromSequences deletes element from elementSequenceMap** -- verify WeakMap entry removed
-- **removeListItems triggers removeFromSequences for removed elements** -- verify integration
-- **removeFromSequences is a no-op for elements not in any Sequence** -- verify no errors
-- **elementSequenceMap is reset on Interact.destroy()** -- verify clean state
-- **MutationObserver removal triggers removeGroups on Sequence** -- verify end-to-end flow
-
-## Data Flow
-
-```mermaid
-flowchart TD
- subgraph registration [Registration -- on add/addListItems]
- R1["Interact.getSequence()"] --> R2["_registerSequenceElements()"]
- R3["Interact.addToSequence()"] --> R2
- R2 --> R4["elementSequenceMap.set(element, sequences)"]
- end
-
- subgraph removal [Removal -- on element removed]
- A[DOM: element removed] --> B[MutationObserver]
- B --> C["_childListChangeHandler()"]
- C --> D["removeListItems(elements)"]
- D --> E["module.remove(element)\n(handler cleanup)"]
- D --> F["Interact.removeFromSequences(elements)"]
- F --> G["elementSequenceMap.get(element)\n=> Set of Sequences -- O(1)"]
- G --> H["sequence.removeGroups(predicate)"]
- H --> I[Cancel matched animations]
- H --> J[Remove from animationGroups]
- H --> K[Remove from timingOptions]
- H --> L[Remove from animations]
- H --> M["Recalculate offsets (applyOffsets)"]
- F --> N["elementSequenceMap.delete(element)"]
- end
-```
diff --git a/.cursor/plans/sequences_feature_tests_e12d5b15.plan.md b/.cursor/plans/sequences_feature_tests_e12d5b15.plan.md
deleted file mode 100644
index ece558db..00000000
--- a/.cursor/plans/sequences_feature_tests_e12d5b15.plan.md
+++ /dev/null
@@ -1,211 +0,0 @@
----
-name: Sequences Feature Tests
-overview: Create comprehensive test suites for the Sequences feature across both `@wix/motion` and `@wix/interact` packages, covering the Sequence class, getSequence function, AnimationGroup.applyOffset, config parsing, add/remove flows, listContainer interactions, and sequence caching.
-todos:
- - id: skeleton-motion
- content: 'Create test file skeletons with describe/test titles for motion package: Sequence.spec.ts, applyOffset tests in AnimationGroup.spec.ts, getSequence.spec.ts'
- status: completed
- - id: skeleton-interact
- content: 'Create test file skeleton with describe/test titles for interact package: sequences.spec.ts (suites A-G)'
- status: completed
- - id: impl-sequence-class
- content: 'Implement Sequence.spec.ts tests: constructor, offset calculation, applyOffsets, inherited playback API, onFinish'
- status: completed
- - id: impl-apply-offset
- content: Implement applyOffset() tests in AnimationGroup.spec.ts
- status: completed
- - id: impl-get-sequence
- content: 'Implement getSequence.spec.ts tests: AnimationGroupArgs[] flow, options forwarding, edge cases'
- status: completed
- - id: impl-interact-config
- content: 'Implement sequences.spec.ts Suite A: config parsing tests'
- status: completed
- - id: impl-interact-source
- content: 'Implement sequences.spec.ts Suite B: sequence processing from source element via add()'
- status: completed
- - id: impl-interact-target
- content: 'Implement sequences.spec.ts Suite C: cross-element sequence processing via addEffectsForTarget'
- status: completed
- - id: impl-interact-list
- content: 'Implement sequences.spec.ts Suite D: sequence with listContainer -- add, addListItems, remove flows'
- status: completed
- - id: impl-interact-cleanup
- content: 'Implement sequences.spec.ts Suite E: removal and cleanup tests'
- status: completed
- - id: impl-interact-cache
- content: 'Implement sequences.spec.ts Suite F: Interact.getSequence caching tests'
- status: completed
- - id: impl-interact-mql
- content: 'Implement sequences.spec.ts Suite G: media query condition tests on sequences'
- status: completed
-isProject: false
----
-
-# Sequences Feature Test Plan
-
-## Phase 1: Motion Package Tests
-
-### 1.1 Create `packages/motion/test/Sequence.spec.ts`
-
-Unit tests for the `Sequence` class in `[packages/motion/src/Sequence.ts](packages/motion/src/Sequence.ts)`. Follow the same `createMockAnimation` pattern from `[packages/motion/test/AnimationGroup.spec.ts](packages/motion/test/AnimationGroup.spec.ts)`.
-
-**Test suites:**
-
-- **Constructor**
- - creates Sequence with empty groups array
- - creates Sequence from multiple AnimationGroups
- - flattens all child animations into parent `animations` array
- - stores `animationGroups` reference
- - defaults: delay=0, offset=0, offsetEasing=linear
- - accepts custom delay, offset, and offsetEasing function
- - resolves named offsetEasing string (e.g. `'quadIn'`) via `getJsEasing`
- - resolves cubic-bezier offsetEasing string
- - falls back to linear for invalid/unknown offsetEasing string
-- **Offset calculation (calculateOffsets)**
- - single group returns [0]
- - linear easing with 5 groups and offset=200 produces [0, 200, 400, 600, 800]
- - quadIn easing with 5 groups and offset=200 produces [0, 50, 200, 450, 800] (spec example)
- - sineOut easing produces expected non-linear offsets
- - floors fractional offsets via `| 0`
-- **applyOffsets (via ready promise)**
- - applies delay + calculated offset to each group via `group.applyOffset()`
- - skips `applyOffset` when additionalDelay is 0
- - waits for all group ready promises before applying offsets
-- **Inherited playback API (from AnimationGroup)**
- - `play()` plays all flattened animations
- - `pause()` pauses all flattened animations
- - `reverse()` reverses all flattened animations
- - `cancel()` cancels all flattened animations
- - `setPlaybackRate()` sets rate on all flattened animations
- - `playState` returns from first animation
-- **onFinish (overridden)**
- - calls callback when all animation groups finish
- - does not call callback if any group's `finished` rejects
- - logs warning on interrupted animation
- - handles empty groups array
-
-### 1.2 Add `applyOffset` tests to `packages/motion/test/AnimationGroup.spec.ts`
-
-Add a new `describe('applyOffset()')` section:
-
-- adds offset to each animation's effect delay via `updateTiming`
-- accumulates with existing delay
-- skips animations with no effect
-- handles empty animations array
-
-### 1.3 Create `packages/motion/test/getSequence.spec.ts`
-
-Tests for the `getSequence()` function in `[packages/motion/src/motion.ts](packages/motion/src/motion.ts)`. Must mock `getAnimation` / `getWebAnimation` as done in `[packages/motion/test/motion.spec.ts](packages/motion/test/motion.spec.ts)`.
-
-**Test suites:**
-
-- **AnimationGroupArgs[] flow**
- - creates Sequence with one AnimationGroup per resolved target element
- - handles a single entry with HTMLElement target
- - handles a single entry with HTMLElement[] target (each element becomes its own group)
- - handles a single entry with string selector target via `querySelectorAll`
- - handles a single entry with null target (passed through to getAnimation)
- - creates Sequence with one group per entry
- - each entry independently resolves its target
-- **Options forwarding**
- - passes SequenceOptions (delay, offset, offsetEasing) to Sequence constructor
- - passes context.reducedMotion to getAnimation
-- **Edge cases**
- - skips entries where getAnimation returns non-AnimationGroup
- - returns Sequence with empty groups when all entries fail
-
-## Phase 2: Interact Package Tests
-
-### 2.1 Create `packages/interact/test/sequences.spec.ts`
-
-Integration tests for sequence handling in the interact package. Follow the mock patterns from `[packages/interact/test/web.spec.ts](packages/interact/test/web.spec.ts)` with the `@wix/motion` mock, but also mock `getSequence` to return a mock Sequence object.
-
-The `@wix/motion` mock needs to be extended to include:
-
-```typescript
-getSequence: vi.fn().mockReturnValue({
- play: vi.fn(), cancel: vi.fn(), onFinish: vi.fn(),
- pause: vi.fn(), reverse: vi.fn(), progress: vi.fn(),
- persist: vi.fn(), isCSS: false, playState: 'idle',
- ready: Promise.resolve(), animations: [], animationGroups: [],
-}),
-```
-
-**Suite A: Config parsing (parseConfig via Interact.create)**
-
-- parses inline sequence on interaction with `effects` array
-- parses `sequenceId` reference from `config.sequences`
-- merges inline overrides onto referenced sequence
-- auto-generates sequenceId when not provided
-- warns when referencing unknown sequenceId
-- caches sequences in `dataCache.sequences`
-- stores sequence effects in `interactions[target].sequences` for cross-element targets
-- does not create cross-element entry when sequence effect targets same key as source (only `_processSequences` handles it)
-- handles interaction with sequences but no effects (effects array is omitted/empty)
-
-**Suite B: Sequence processing via `add()` -- source element**
-
-- creates Sequence when source element is added with viewEnter trigger
-- creates Sequence when source element is added with click trigger
-- passes correct AnimationGroupArgs built from effect definitions
-- resolves effectId references from config.effects
-- skips sequence when target controller is not yet registered
-- does not duplicate sequence on re-add (caching via `addedInteractions`)
-- passes pre-created Sequence as `animation` option to trigger handler
-- passes selectorCondition to handler options
-- silently skips unresolved sequenceId reference at runtime (`_processSequences` returns early)
-- skips entire sequence when any effect target element is missing (`_buildAnimationGroupArgsFromSequence` returns null)
-
-**Suite C: Sequence processing via `addEffectsForTarget()` -- cross-element**
-
-- creates Sequence when target element is added after source
-- creates Sequence when source element is added after target
-- handles sequences where effects target different keys
-- skips variation when interaction-level MQL does not match and falls through to next variation
-- skips when source controller is not yet registered
-- `addEffectsForTarget` returns true when sequences exist even without effects
-
-**Suite D: Sequence with listContainer**
-
-- creates Sequence for each list item when source has listContainer
-- creates new Sequence per `addListItems` call with unique cache key (each call uses `${cacheKey}::${generateId()}`)
-- handles removing list items (via `removeListItems`) and subsequent re-add
-- processes sequence effects from listContainer elements
-- does not create duplicate sequence when list items overlap with existing
-- skips sequence when listElements provided but no effects matched the listContainer (`usedListElements` guard)
-- cross-element target: creates new Sequence per `addListItems` call for target sequences
-
-**Suite E: Sequence removal and cleanup**
-
-- `remove()` cleans up sequence cache entries for the removed key
-- `Interact.destroy()` clears sequenceCache
-- `deleteController()` removes sequence-related `addedInteractions` entries
-- `clearInteractionStateForKey` removes sequenceCache entries by key prefix (`${key}::seq::`)
-
-**Suite F: Interact.getSequence caching**
-
-- returns cached Sequence for same cacheKey
-- creates new Sequence for different cacheKey
-- passes sequenceOptions and animationGroupArgs to motion's `getSequence`
-
-**Suite G: Media query conditions on sequences**
-
-- skips sequence when sequence-level condition does not match
-- skips individual effect within sequence when effect-level condition does not match
-- sets up media query listener for sequence conditions
-- sets up media query listener for effect-level conditions within sequence
-
-## Phase 3: Implementation Approach
-
-Each phase above will be implemented in order:
-
-1. First create all spec files with `describe`/`test` **skeletons only** (titles, no bodies)
-2. Implement motion package tests (Sequence.spec.ts, applyOffset in AnimationGroup.spec.ts, getSequence.spec.ts)
-3. Implement interact package sequence tests (sequences.spec.ts) suite by suite
-
-### Key mock patterns to reuse
-
-- `createMockAnimation()` from `AnimationGroup.spec.ts` for motion tests
-- `vi.mock('@wix/motion', ...)` from `web.spec.ts` for interact tests, extended with `getSequence`
-- `InteractionController` + `add()` helper for interact element setup
-- `addListItems` import for list container tests
diff --git a/packages/interact/dev/sequences-spec.md b/packages/interact/dev/sequences-spec.md
deleted file mode 100644
index 6d8e4559..00000000
--- a/packages/interact/dev/sequences-spec.md
+++ /dev/null
@@ -1,176 +0,0 @@
-# Sequences
-
-This is a proposal for supporting sequenced effects (also known as “timelines”) for Interact that can be declared using the Config.
-
-# Technical Design
-
-## Config spec
-
-- A Sequence is a list of `Effect`s managed by a single trigger/timeline.
-- `Effect`s in a `Sequence` are applied in their specified order inside `Interaction.sequence.effects`.
-- Reusable `Sequence`s will be declared using a new `InteractConfig.sequences` property, which is a map of Sequence declarations by a unique key.
-- `Sequence`s can be defined on an `Interaction` using a new `Interaction.sequences` property which is a list of `Sequence`s.
-- Each Sequence will have an `effects` property which contains its child Effects.
-- A `Sequence` does not have a `key` property, nor any of the other element targeting-related properties, since it by itself is not tied to an element.
-- The `Effect`s inside a `Sequence` are the objects that define that related target.
-
-## The new `sequences` property
-
-```ts
-/**
- * Reusable Sequence declarations on the InteractConfig top-level
- */
-type InteractConfig = {
- sequences: {[key: string]: Sequence};
- //...
-}
-
-/**
- * Sequence definitions on each Interaction
- * Like `effects`, can either reference a declaration using sequenceId
- * Or specify values inline, or both and inlined overrides referenced declarations
- */
-type Interaction = {
- sequences: Sequence[];
- //...
-};
-
-/**
- * The SequenceOptions type
- */
-type SequenceOptions = {
- delay?: number; // default 0
- offset?: number; // default 0
- offsetEasing?: string | (p: number) => number; // linear
- sequenceId?: string; // provided or generated automatically
-};
-
-/**
- * The SequenceConfig type
- */
-type SequenceConfig = SequenceOptions & {
- effects: Effect[];
-};
-```
-
-## The `Sequence.delay`
-
-- A fixed offset of milliseconds to delay the playing of the entire Sequence
-- Defaults to `0`
-
-## The `Sequence.offset`
-
-- A fixed amount of milliseconds to multiply the result of the `easing` function
-- Defaults to `0`
-
-## The `Sequence.offsetEasing`
-
-- Either a JS function or a valid CSS `easing` value, or a valid `easing` name in `@wix/motion` library
- - A JS function takes a `number` from 0 to 1\.
- - An `easing` value, either valid from CSS, or in `@wix/motion`, will be translated to the corresponding function in JS or a CSS `calc()`.
-- The mapping of each offset using the easing function as done as follows:
-
-```javascript
-// `indices` is the array of indices from 0 to Length-1
-// `easing` is the easing function
-// `offset` is the `sequence.offset` property
-const last = indices.at(-1);
-indices.map((n) => (easing(n / last) * last * offset) | 0); // | 0 is basically flooring
-```
-
-### Easing examples
-
-```javascript
-const items = [0, 1, 2, 3, 4];
-const offset = 200;
-
-const linear = (t) => t;
-// 0, 200, 400, 600, 800
-
-const quadIn = (t) => t ** 2;
-// 0, 50, 200, 450, 800
-
-const sinOut = (t) => Math.sin((t * Math.PI) / 2);
-// 0, 306, 565, 739, 800
-```
-
-## The `Sequence.align`
-
-- **Ignore for now \- DO NOT implement**
-- Specifies how to align the Effects inside the Sequence:
- - `start` aligns to the beginning
- - `end` aligns to the end
- - `sequence` aligns each effect’s start to the end of its preceding effect
- - `sequence-reverse` is same as `sequence` but starts from the last effect backwards
-
-# Effect on Effects’ `delay`
-
-- In initial phase this feature should only apply to `keyframeEffect`s and `namedEffect`s \- where we generate Web or CSS animations
-- The result of calculated offset should be added to the Effect’s specified `delay`
-- If an Effect is removed (e.g. when an Effect’s `condition` stops matching the state of its environment and needs to be removed) it should propagate to the corresponding Sequence to update the calculated delays
-- If an entire Sequence is removed we should try to remove it completely without a significant overhead of propagating each Effect being removed.
-
-# Implementation
-
-- Create a new `Sequence` class in `@wix/motion` package that manages a list of `AnimationGroup` instances.
-- A `Sequence` instance manages its own playback, similar to `AnimationGroup`, only difference is its `animations` property holds `AnimationGroup` instances. Therefor, it should extend `AnimationGroup` and have a similar API.
-- Note that `Sequence` does not have a `target`, so all of its API endpoints that involve an element target should be written accordingly, or not exist if not relevant.
-- In the `@eix/interact` package `Sequence`s will be created from an `InteractConfig` for every declaration inside `Interaction.sequences`.
-
-# Appendix
-
-## A CSS solution in a futuristic world where CSS math functions are widely supported
-
-- The index of each Effect in the Sequence and count of Effects in the Sequence are set on each target element.
-- Generated `animation-delay` should be a `calc()` that includes the effect’s `delay` \+ the generated staggering offset as follows:
-
-```css
-@property --interact-seq-c {
- syntax: '';
- initial-value: 1;
- inherits: false;
-}
-
-@property --interact-seq-i {
- syntax: '';
- initial-value: 0;
- inherits: false;
-}
-
-.target {
- --_interact-delay: calc(
- pow(var(--interact-seq-i) / var(--interact-seq-c), 2)
- ); /* quadIn - this is here for readability, don't actually have to add as a separate property */
- animation-delay: calc(
- effectDelay + var(--_interact-delay) * var(--interact-seq-c) *
- );
-}
-```
-
-```javascript
-// According to initial design
-{
- interactions: [{
- trigger: 'viewEnter',
- sequence: {
- offset: 150,
- offsetEasing: 'ease-out'
- },
- effects: [...]
- }]
-}
-
-// According to alternative design
-{
- interactions: [{
- trigger: 'viewEnter',
- sequences: [{
- offset: 150,
- offsetEasing: 'ease-out',
- effects: [{
-
- }]
- }]
- }]
-}
-```
diff --git a/packages/interact/rules/MASTER-CLEANUP-PLAN.md b/packages/interact/rules/MASTER-CLEANUP-PLAN.md
deleted file mode 100644
index dc9c9fb5..00000000
--- a/packages/interact/rules/MASTER-CLEANUP-PLAN.md
+++ /dev/null
@@ -1,286 +0,0 @@
-# Master Cleanup Plan: `@wix/interact` Rule Files
-
-Synthesized from three independent audits. Filtered through current best practices for documentation consumed by LLMs.
-
----
-
-## Guiding Principles (Why This Matters)
-
-These rules are the primary context fed to AI agents generating `@wix/interact` code. Every wasted token, every duplicated section, every drift between files directly degrades output quality. The standards we apply:
-
-1. **Token efficiency over completeness.** LLMs have broad web animation knowledge. Rules should focus exclusively on `@wix/interact`-specific contracts, constraints, and non-obvious patterns — not general CSS or animation education.
-2. **Single source of truth, always.** Duplicated content drifts. When it drifts, models get contradictory instructions and hedge or hallucinate.
-3. **Each file must be usable standalone.** The MCP loads one file at a time based on topic. A model fetching `"click"` gets only `click.md` — it will not automatically also receive `full-lean.md`. Each trigger file must therefore contain all `@wix/interact`-specific constraints the model needs for that trigger, including brief inline summaries of schema concepts it depends on.
-4. **Correctness over breadth.** A contradiction is worse than a gap. Fix all confirmed conflicts before any structural changes.
-5. **Describe when/how, not what/why at length.** A rule like `"alternate plays forward on enter, reverses on leave"` is worth keeping. A paragraph re-explaining what `IntersectionObserver` does is not.
-6. **Delete, don't redirect.** Generic web animation advice should be deleted entirely — not moved to a shared file or linked. The model already knows it. Cross-file links are useful for human navigation only; they are not a content delivery mechanism for the MCP.
-
----
-
-## Current State
-
-- 8 files, ~8,000+ lines
-- ~30–40% estimated duplication
-- 5 confirmed typos/errors
-- 2 confirmed correctness contradictions
-- 1 confirmed schema inconsistency between files (`listItemSelector` vs `selector`)
-
----
-
-## Phase 1 — Fix Correctness Issues (do first, no structural work yet)
-
-These are the highest-risk problems. A model following a contradictory rule produces wrong code.
-
-### 1.1 Resolve `keyframeEffect` + `pointerMove` conflict
-
-**Conflict:**
-
-- `full-lean.md` lines 366 and 403: "do NOT use `keyframeEffect` with `pointerMove` because pointer progress is two-dimensional"
-- `full-lean.md` line 173: documents `axis?: 'x' | 'y'` param that collapses 2D progress to one axis for keyframes
-- `pointermove.md`: Rules 10–11 document `keyframeEffect` + `axis` as a valid first-class pattern
-
-**Resolution:** `full-lean.md` lines 366 and 403 are incomplete. The `axis` parameter exists precisely to make single-axis `keyframeEffect` valid. Update both lines to:
-
-> Avoid `keyframeEffect` with `pointerMove` unless using `params: { axis: 'x' | 'y' }` to map a single pointer axis to linear 0–1 progress.
-
-### 1.2 Resolve `listItemSelector` vs `selector` inconsistency
-
-**Conflict:**
-
-- `full-lean.md` uses `listItemSelector` as the field name
-- `integration.md` uses `selector` for the same concept
-
-**Resolution:** Check the TypeScript type definition. Standardize both files to the correct field name. One of these is currently giving models the wrong API.
-
-### 1.3 Align FOUC constraints
-
-**Conflict:**
-
-- `full-lean.md` correctly restricts `data-interact-initial="true"` to: `viewEnter` trigger + `type: 'once'` + source element = target element
-- `integration.md` gives the same code example but omits the constraints
-
-**Resolution:** Add the full constraints explicitly to `integration.md`'s FOUC section.
-
-### 1.4 Fix `pointermove.md` undefined reference
-
-`pointermove.md` has an example referencing `indicator-effect` which is not defined in the config shown. Fix the example to be self-contained.
-
-### 1.5 Fix all confirmed typos
-
-| File | Line | Fix |
-| ----------------- | ---- | ------------------------------------------------- |
-| `viewenter.md` | 946 | `Guildelines` → `Guidelines` |
-| `viewenter.md` | 980 | `HUge` → `Huge` |
-| `viewprogress.md` | 43 | `effec` → `effect` |
-| `hover.md` | 515 | `same ass` → `same as` |
-| `integration.md` | 195 | `(Pre-built effect library)>` → remove stray `>` |
-| `scroll-list.md` | 272 | `selector: ' .hero-image'` → remove leading space |
-
----
-
-## Phase 2 — Establish Single Source of Truth (via deletion, not links)
-
-### 2.1 Define canonical ownership
-
-Because the MCP loads one file at a time, the "canonical" column means "most complete definition lives here." The "inline mention needed" column means trigger docs that depend on this concept must include a brief self-contained summary — not a link, not a full re-explanation.
-
-| Content | Canonical file | Action in other files | Inline mention needed in |
-| ----------------------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
-| Full type/schema spec (`InteractConfig`, triggers, effects, conditions) | `full-lean.md` | Delete duplicated schema prose from `integration.md` | All trigger docs: keep brief summaries of params they use |
-| Developer setup (install, web, react, CDN, `Interact.create`) | `integration.md` | — | — |
-| FOUC / `generate(config)` | `full-lean.md` + `viewenter.md` | Delete full code block from `integration.md` only | `viewenter.md`: full working example with constraints — it is the most likely file fetched for entrance animations and must be self-contained |
-| `StateParams.method` (`add`/`remove`/`toggle`/`clear`) | `full-lean.md` | — | `click.md`: inline comment on TransitionEffect rule only (`hover.md` has no TransitionEffect rule so no mention needed there) |
-| Target cascade resolution | `full-lean.md` | — | Any trigger doc showing cross-targeting examples |
-| `Progress` type for `customEffect` with `pointerMove` | `pointermove.md` | Delete duplicate definition from `full-lean.md` | — |
-| `fill: 'both'` for `viewProgress` | `full-lean.md` | — | `viewprogress.md`: keep inline (model fetching viewprogress won't have full-lean) |
-| `registerEffects` | currently only in `integration.md` | Add to `full-lean.md` | — |
-| Generic perf/UX/a11y advice | nowhere — delete entirely | Remove from all trigger docs | — |
-
-### 2.2 Reduce `integration.md`
-
-`integration.md` is a developer-facing onboarding guide — not a schema reference. Effect type syntax belongs in `full-lean.md`; the 3 end-to-end examples at the bottom of `integration.md` already demonstrate effect types in context, which is more useful for onboarding than standalone snippets.
-
-- **Keep:** install steps, web/react setup snippets, `Interact.create` usage, HTML/JSX element wrappers, `registerEffects` setup, trigger overview table, 3 working examples (hover, viewEnter, click), FOUC rules
-- **Delete:** full effect type taxonomy (`keyframeEffect`, `TransitionEffect`, scroll/mouse effect snippets) — covered by `full-lean.md` and shown contextually in the examples
-
-### 2.3 Strip generic best-practices content from all trigger docs
-
-Do **not** create a shared `best-practices.md`. Instead, apply this filter to every trigger doc's Best Practices section:
-
-**Delete (model already knows this):**
-
-- "Use `transform`, `opacity`, `filter` for hardware acceleration"
-- "Avoid animating layout properties"
-- "`will-change` for complex animations"
-- "Keep animations subtle"
-- "Ensure content remains readable"
-- "Progressive enhancement"
-- Generic "Respect `prefers-reduced-motion`" without `@wix/interact`-specific guidance
-
-**Keep (interact-specific, non-obvious):**
-
-- `@wix/interact` conditions API for `prefers-reduced-motion`: how to wire it via `conditions` field. **Clarification:** condition IDs are user-defined strings — examples must always show the full `conditions` config map (with `type` and `predicate`) alongside the interaction that references them, not just the ID strings in isolation
-- Trigger-specific timing constraints (e.g. click: 100–500ms, hover: 100–400ms)
-- Trigger-specific gotchas (e.g. viewEnter: don't animate source and target as the same element with `repeat` type)
-- `pointermove`: cache DOM queries outside `customEffect` callbacks
-- `viewprogress`: stacking contexts freeze ViewTimeline — this belongs in `full-lean.md` general guidelines (already there) and should be removed from `viewprogress.md` best practices
-
-**Estimated line savings: ~250 lines across trigger docs.**
-
----
-
-## Phase 3 — Restructure `viewprogress.md`
-
-This is the single largest structural problem. 9 rules are a 3×3 matrix with near-identical patterns:
-
-| | `namedEffect` | `keyframeEffect` | `customEffect` |
-| ------------------- | ------------- | ---------------- | -------------- |
-| Parallax/Continuous | Rule 1 | Rule 4 | Rule 7 |
-| Entry | Rule 2 | Rule 5 | Rule 8 |
-| Exit | Rule 3 | Rule 6 | Rule 9 |
-
-Every rule repeats the same config skeleton. Variable lists from Rule 5 onward explicitly say "Other variables same as Rule 1" — a direct admission of duplication.
-
-**Why tables outperform the 9-rule format for LLM consumption:**
-
-The 9-rule matrix looks comprehensive to humans but is inefficient for models. Once a model has seen the config pattern once, repeating it 8 more times with minor substitutions adds no information — it just consumes context tokens. The replacement structure (1 template + 2 lookup tables + 4 examples) performs better because:
-
-- **Decision tables are scannable.** A model resolving "I need an entry animation" can map scenario → effect type → range names in one pass through the table, rather than pattern-matching a natural-language rule description to its task.
-- **Fewer examples, correctly chosen, generalize better.** 4 curated examples (one per effect type + one non-obvious multi-range pattern) teach the model to compose, rather than encouraging copy-paste from the closest-matching rule.
-- **Token efficiency directly affects output quality.** Fewer tokens spent on redundant patterns means more context budget for the actual user task.
-
-The trade-off: the old format had a safer floor for weaker models that benefit from rote examples. The new format has a higher ceiling for capable models (Claude, GPT-4, Gemini) that generalize well from clean, structured docs — which is the target audience for this library.
-
-### Target structure for `viewprogress.md`
-
-**Section 1: Core Concept** (~5 lines)
-One sentence on what `viewProgress` does (scroll-driven via `ViewTimeline`). No animation education.
-
-**Section 2: Config Template** (1 canonical pattern block)
-Single pattern showing all `viewProgress`-relevant fields with placeholders.
-
-**Section 3: Effect Type Selection** (table)
-
-| Scenario | Effect type | Notes |
-| -------------------------- | ---------------- | -------------------------------- | ----- | -------------------- |
-| Use a scroll preset | `namedEffect` | Preferred; requires `range: 'in' | 'out' | 'continuous'` option |
-| Custom CSS animation | `keyframeEffect` | Full keyframe control |
-| DOM/canvas/dynamic content | `customEffect` | Last resort; keep callback lean |
-
-**Section 4: Range Reference** (table)
-
-| Intent | `rangeStart.name` | `rangeEnd.name` | Typical offsets |
-| ------------------------- | ----------------- | --------------- | --------------- |
-| Element entering viewport | `entry` | `entry` | 0–60% |
-| Element exiting viewport | `exit` | `exit` | 0–60% |
-| Full element traversal | `cover` | `cover` | 0–100% |
-| While fully in viewport | `contain` | `contain` | 0–100% |
-
-Include the offset semantics note (positive = forward along scroll axis) — once, here only.
-
-**Section 5: Named Scroll Effects Reference** (condensed list)
-The scroll preset names currently buried in Rule 1 variables — one list, not repeated across rules.
-
-**Section 2 note: Config Template placeholders**
-Do not include a `direction` placeholder. `direction` is a preset-specific option (not a standard field), its valid values differ per preset, and listing generic values would be incomplete and misleading. The template uses `[NAMED_EFFECT]` with a note: only use preset-specific options you have documentation for; omit and rely on defaults otherwise.
-
-**Section 6: Examples** (4 total)
-
-- `namedEffect` parallax
-- `keyframeEffect` custom entrance
-- `customEffect` scroll counter
-- **Multi-range (entry + exit on the same element)** — non-obvious pattern requiring two effects with the same `key` but different range scopes. Without a dedicated example, models can infer it from the tables but may get the fill/easing direction wrong. The cost (~40 lines) is worth the reliability gain.
-
-**Section 7: Advanced Patterns** — keep existing section as-is (genuinely unique content)
-
-**Section 8: Best Practices** — interact-specific delta only (per Phase 2.3 filter above)
-
-**Estimated line savings: ~~600 lines (~~55% reduction of the file).**
-
----
-
-## Phase 4 — Reduce `scroll-list.md`
-
-This file's genuine value is its list-specific patterns. Everything else repeats `viewprogress.md`.
-
-### Keep (unique to lists)
-
-- Sticky container/item/content hierarchy explanation
-- `listContainer` + `listItemSelector` (or `selector`) setup and rules
-- Why `contain` range fits sticky container animations specifically
-- Stagger pattern using shared `effectId` in the effects registry
-- `customEffect` pattern for per-item dynamic content
-- Responsive list animations section with a full `conditions` config map example (same pattern as other trigger docs — condition IDs are user-defined, must always be shown alongside their `type`/`predicate` definition)
-
-### Delete (generic, already in `viewprogress.md` or model already knows it)
-
-- Range name semantics — already in `viewprogress.md` after Phase 3
-- Effect type taxonomy — already in `viewprogress.md` after Phase 3
-- Generic `fill: 'both'` explanation
-- Generic performance/UX/a11y best practices (per Phase 2.3 filter)
-
-**Estimated line savings: ~200 lines.**
-
----
-
-## Phase 5 — Trigger Doc Standardization
-
-Minor but important for model consistency. Models that see consistent structure learn to pattern-match faster across files.
-
-### Issues to fix
-
-- `hover.md` title: "Hover Trigger Rules" — missing `for @wix/interact` (all others have it)
-- `hover.md` has no Accessibility section (the only trigger doc missing it entirely) — add the interact-specific `conditions`-based reduced motion guidance
-- Variable placeholder naming is inconsistent: `[SOURCE_KEY]` (viewprogress, pointermove) vs `[SOURCE_IDENTIFIER]` (click, hover) for the same concept
-- `hover.md` Rules 2 and 3 overlap heavily (both are `alternate` pattern, one with `namedEffect`, one with `keyframeEffect`) — collapse into one rule with two examples
-- `click.md` shows only `method: 'toggle'` for `TransitionEffect` — add brief mention that `add`, `remove`, `clear` also exist (already defined in `full-lean.md`, but models reading only the trigger doc will miss it)
-- `hover.md` does **not** get a `method` mention — `hover.md` has no `TransitionEffect` rule, so adding a method summary there would be orphaned with no anchor
-
-### Fixes
-
-- Standardize title format: `# [Trigger] Trigger Rules for @wix/interact`
-- Add Accessibility section to `hover.md` (interact-specific content only)
-- Standardize placeholder names across all trigger docs: use `[SOURCE_KEY]` / `[TARGET_KEY]` everywhere
-- Collapse `hover.md` Rules 2+3 into one rule with two examples
-- Add one-line mention of `add`/`remove`/`clear` methods to `click.md` TransitionEffect rule
-- Remove trailing "These rules provide comprehensive coverage..." footers from `click.md`, `viewenter.md`, `viewprogress.md`, `pointermove.md`
-
----
-
-## Final File Structure
-
-```
-packages/interact/rules/
-├── full-lean.md ← canonical spec: schema, types, all trigger params, effect rules, general gotchas
-│ changes: fix keyframeEffect+axis note, add registerEffects, remove duplicate Progress type
-├── integration.md ← onboarding only: setup, 3 working examples, trigger overview table
-│ changes: reduce from ~370 → ~150 lines by deleting schema/effect prose
-├── click.md ← trigger patterns + examples + interact-specific best practices
-├── hover.md ← trigger patterns + examples; add a11y section; collapse rules 2+3
-├── viewenter.md ← trigger patterns + examples; full FOUC example with constraints
-├── viewprogress.md ← 1 template + 2 tables + 4 examples + advanced patterns
-│ changes: remove 9-rule matrix (~600 lines); no direction placeholder in template
-├── scroll-list.md ← list-specific only (sticky hierarchy, stagger, list context)
-│ changes: delete generic scroll/range/effect content (~200 lines)
-└── pointermove.md ← keep Core Concepts (genuine unique value); trim best practices
-```
-
----
-
-## Execution Order
-
-| # | Action | Files affected | Est. lines removed | Risk |
-| --- | ------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------ | ------------------ |
-| 1 | Fix correctness: `keyframeEffect`/`pointerMove` conflict | `full-lean.md` | — | High if skipped |
-| 2 | Fix correctness: `listItemSelector` vs `selector` | `full-lean.md`, `integration.md` | — | High if skipped |
-| 3 | Fix correctness: FOUC constraints alignment | `integration.md` | — | High if skipped |
-| 4 | Fix 6 typos + undefined `pointermove.md` reference | all | — | Low effort, do now |
-| 5 | Delete generic best-practices content from all trigger docs | all trigger docs | ~250 | Low |
-| 6 | Refactor `viewprogress.md`: 1 template + 2 tables + 4 examples (added multi-range) | `viewprogress.md` | ~600 | Medium |
-| 7 | Reduce `scroll-list.md`: delete generic scroll/range/effect content | `scroll-list.md` | ~200 | Low |
-| 8 | Reduce `integration.md`: delete schema/effect prose | `integration.md` | ~150 | Low |
-| 9 | Add `registerEffects` to `full-lean.md`; remove duplicate `Progress` type | `full-lean.md` | — | Low |
-| 10 | Standardize titles, placeholders, collapse `hover.md` rules 2+3, add missing `method` mention, remove footers | all | ~50 | Very low |
-
-**Estimated total reduction: ~~1,250 lines (~~15% of corpus), with zero loss of `@wix/interact`-specific information.**
-The remaining content will be denser, more accurate, and cheaper for models to consume.
diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md
index 17805bdc..f6cc80fc 100644
--- a/packages/interact/rules/click.md
+++ b/packages/interact/rules/click.md
@@ -1,653 +1,222 @@
# Click Trigger Rules for @wix/interact
-These rules help generate click-based interactions using the `@wix/interact` library. Click triggers respond to mouse click events and support multiple behavior patterns for different user experience needs.
+This document contains rules for generating click-triggered interactions in `@wix/interact`.
-## Rule 1: Click with TimeEffect and Alternate Pattern
+**CRITICAL — Accessible click**: Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space).
-**Use Case**: Toggle animations that play forward on first click and reverse on subsequent clicks (e.g., menu toggles, accordion expand/collapse, modal open/close)
+## Table of Contents
-**When to Apply**:
-
-- When you need reversible animations
-- For toggle states that should animate back to original position
-- When creating expand/collapse functionality
-- For modal or sidebar open/close animations
-
-**Pattern**:
-
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'click',
- params: {
- type: 'alternate'
- },
- effects: [
- {
- key: '[TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
- fill: 'both',
- reversed: [INITIAL_REVERSED_BOOL],
- duration: [DURATION_MS],
- easing: '[EASING_FUNCTION]',
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[SOURCE_KEY]`: Unique identifier for clickable element. Should equal the value of the `data-interact-key` attribute on the wrapping ``.
-- `[TARGET_KEY]`: Unique identifier for animated element (can be same as `[SOURCE_KEY]` for self-targeting, or different for cross-targeting).
-- `[EFFECT_TYPE]`: Either `namedEffect` or `keyframeEffect`
-- `[EFFECT_DEFINITION]`: Named effect object (e.g., { type: 'SlideIn', ...params }, { type: 'FadeIn', ...params }) or keyframe object (e.g., { name: 'custom-fade', keyframes: [{ opacity: 0 }, { opacity: 1 }] }, { name: 'custom-slide', keyframes: [{ transform: 'translateX(-100%)' }, { transform: 'translateX(0)' }] })
-- `[INITIAL_REVERSED_BOOL]`: Optional boolean value indicating whether the first toggle should play the reversed animation.
-- `[DURATION_MS]`: Animation duration in milliseconds (typically 200-500ms for clicks)
-- `[EASING_FUNCTION]`: Timing function ('ease-out', 'ease-in-out', or cubic-bezier)
-- `[UNIQUE_EFFECT_ID]`: Optional unique identifier for animation chaining
-
-**Example - Menu Toggle**:
-
-```typescript
-{
- key: 'hamburger-menu',
- trigger: 'click',
- params: {
- type: 'alternate'
- },
- effects: [
- {
- key: 'mobile-nav',
- namedEffect: {
- type: 'SlideIn',
- direction: 'left'
- },
- fill: 'both',
- reversed: true,
- duration: 300,
- easing: 'ease-out',
- effectId: 'mobile-nav-toggle'
- }
- ]
-}
-```
-
-**Example - Accordion Expand**:
-
-```typescript
-{
- key: 'accordion-header',
- trigger: 'click',
- params: {
- type: 'alternate'
- },
- effects: [
- {
- key: 'accordion-content',
- keyframeEffect: {
- name: 'accordion',
- keyframes: [
- { clipPath: 'inset(0 0 100% 0)', opacity: '0' },
- { clipPath: 'inset(0 0 0 0)', opacity: '1' }
- ]
- },
- fill: 'both',
- reversed: true,
- duration: 400,
- easing: 'ease-in-out'
- }
- ]
-}
-```
+- [Rule 1: keyframeEffect / namedEffect with PointerTriggerParams](#rule-1-keyframeeffect--namedeffect-with-pointertriggerparams)
+- [Rule 2: transition / transitionProperties with StateParams](#rule-2-transition--transitionproperties-with-stateparams)
+- [Rule 3: customEffect with PointerTriggerParams](#rule-3-customeffect-with-pointertriggerparams)
+- [Rule 4: Sequences](#rule-4-sequences)
---
-## Rule 2: Click with TimeEffect and State Pattern
-
-**Use Case**: Animations that can be paused and resumed with clicks (e.g., video controls, loading animations, slideshow controls)
+## Rule 1: keyframeEffect / namedEffect with PointerTriggerParams
-**When to Apply**:
+Use `keyframeEffect` or `namedEffect` when the click should play an animation (CSS or WAAPI). Pair with `PointerTriggerParams` to control playback behavior.
-- When you need play/pause functionality
-- For controlling ongoing animations
-- When users should be able to interrupt and resume animations
-- For interactive media controls
+**CRITICAL:** Always include `fill: 'both'` for `type: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `type: 'once'`, use `fill: 'backwards'` or `fill: 'none'`.
-**Pattern**:
+**Multiple effects:** The `effects` array can contain multiple effects — all share the same click trigger and fire together. Use this to animate different targets from a single click event.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'click',
params: {
- type: 'state'
+ type: '[EVENT_TRIGGER_TYPE]'
},
effects: [
{
key: '[TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
- fill: 'both',
- reversed: [INITIAL_REVERSED_BOOL],
- duration: [DURATION_MS],
- easing: '[EASING_FUNCTION]',
- iterations: [ITERATION_COUNT],
- alternate: [ALTERNATE_BOOL],
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-- `[ITERATION_COUNT]`: Number of iterations or Infinity for infinite looping animations
-- `[ALTERNATE_BOOL]`: Optional boolean value indicating whether to alternate/toggle the playing direction of the animation on each iterations. Relevant only if `[ITERATION_COUNT]` is not 1.
-- Other variables same as Rule 1
-
-**Example - Loading Spinner Control**:
-
-```typescript
-{
- key: 'loading-control',
- trigger: 'click',
- params: {
- type: 'state'
- },
- effects: [
- {
- key: 'spinner',
+ // --- pick ONE of the two effect types ---
keyframeEffect: {
- name: 'spin',
- keyframes: [
- { transform: 'rotate(0deg)' },
- { transform: 'rotate(360deg)' }
- ]
+ name: '[EFFECT_NAME]',
+ keyframes: [KEYFRAMES],
},
- duration: 1000,
- easing: 'linear',
- iterations: Infinity,
- effectId: 'spinner-rotation'
- }
- ]
-}
-```
-
-**Example - Slideshow Pause**:
-
-```typescript
-{
- key: 'slideshow-toggle',
- trigger: 'click',
- params: {
- type: 'state'
- },
- effects: [
- {
- key: 'slideshow-container',
- namedEffect: { type: 'ShuttersIn' },
- duration: 3000,
- iterations: 10,
- alternate: true,
- effectId: 'slideshow-animation'
- }
- ]
-}
-```
-
----
-
-## Rule 3: Click with TimeEffect and Repeat Pattern
+ // OR
+ namedEffect: [NAMED_EFFECT_DEFINITION],
-**Use Case**: Animations that restart from the beginning each time clicked (e.g., pulse effects, notification badges, emphasis animations)
-
-**When to Apply**:
-
-- When you want fresh animation on each click
-- For attention-grabbing effects
-- When animation should always start from initial state
-- For feedback animations that confirm user actions
-
-**Pattern**:
-
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'click',
- params: {
- type: 'repeat'
- },
- effects: [
- {
- key: '[TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
+ fill: '[FILL_MODE]',
+ reversed: [INITIAL_REVERSED_BOOL],
duration: [DURATION_MS],
easing: '[EASING_FUNCTION]',
delay: [DELAY_MS],
+ iterations: [ITERATIONS],
+ alternate: [ALTERNATE_BOOL],
effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[DELAY_MS]`: Optional delay before animation starts (useful for sequencing)
-- Other variables same as Rule 1
-
-**Example - Button Pulse Feedback**:
-
-```typescript
-{
- key: 'action-button',
- trigger: 'click',
- params: {
- type: 'repeat'
- },
- effects: [
- {
- key: 'action-button',
- keyframeEffect: {
- name: 'button-shadow',
- keyframes: [
- { transform: 'scale(1)', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' },
- { transform: 'scale(1.1)', boxShadow: '0 8px 16px rgba(0,0,0,0.2)' },
- { transform: 'scale(1)', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }
- ]
- },
- duration: 300,
- easing: 'ease-out'
- }
+ },
+ // additional effects targeting other elements can be added here
]
}
```
-**Example - Success Notification**:
+### Variables
-```typescript
-{
- key: 'save-button',
- trigger: 'click',
- params: {
- type: 'repeat'
- },
- effects: [
- {
- key: 'success-badge',
- namedEffect: {
- type: 'BounceIn',
- direction: 'center'
- },
- duration: 600,
- easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
- delay: 300,
- effectId: 'success-feedback'
- }
- ]
-}
-```
+- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). The element that listens for clicks.
+- `[TARGET_KEY]` — identifier matching the element's key on the element that animates. Same as `[SOURCE_KEY]` for self-targeting, or different for cross-targeting.
+- `[EVENT_TRIGGER_TYPE]` — `PointerTriggerParams.type`. One of:
+ - `'alternate'` — plays forward on first click, reverses on next click. Most common for toggles.
+ - `'repeat'` — restarts the animation from the beginning on each click.
+ - `'once'` — plays once on the first click and never again.
+ - `'state'` — resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`).
+- `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.
+- `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`.
+- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.
+- `[FILL_MODE]` - optional. Always `'both'` with `type: 'alternate'` or `'repeat'`, otherwise depends on the effect.
+- `[INITIAL_REVERSED_BOOL]` — optional. `true` to start in the finished state so the entire effect is reversed.
+- `[DURATION_MS]` — animation duration in milliseconds.
+- `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`.
+- `[DELAY_MS]` — optional delay before the effect starts, in milliseconds.
+- `[ITERATIONS]` — optional. Number of iterations, or `Infinity` for continuous loops.
+- `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration (within a single playback). Different from `type: 'alternate'` which alternates per click.
+- `[UNIQUE_EFFECT_ID]` — optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects from the top-level `effects` map.
---
-## Rule 4: Click with State Toggles and TransitionEffects
-
-**Use Case**: CSS property changes that toggle between states (e.g., theme switching, style variations, color changes)
+## Rule 2: transition / transitionProperties with StateParams
-**When to Apply**:
+Use `transition` or `transitionProperties` when the click should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Uses the `transition` CSS property. Pair with `StateParams` to control how the style is applied.
-- When animating CSS properties directly
-- For theme toggles and style switches
-- When you need precise control over CSS transitions
-- For simple property changes without complex keyframes
-
-**Pattern**:
+Use `transition` when all properties share timing. Use `transitionProperties` when each property needs independent `duration`, `delay`, or `easing`.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'click',
params: {
- method: 'toggle' // also: 'add', 'remove', 'clear' — see full-lean.md StateParams
+ method: '[TRANSITION_METHOD]'
},
effects: [
{
key: '[TARGET_KEY]',
+
+ // --- pick ONE of the two transition forms ---
transition: {
duration: [DURATION_MS],
delay: [DELAY_MS],
easing: '[EASING_FUNCTION]',
styleProperties: [
- { name: '[CSS_PROPERTY_1]', value: '[VALUE_1]' },
- { name: '[CSS_PROPERTY_2]', value: '[VALUE_2]' }
+ { name: '[CSS_PROP]', value: '[VALUE]' },
+ // ... more properties
]
},
- effectId: '[UNIQUE_EFFECT_ID]'
+ // OR (when each property needs its own timing)
+ transitionProperties: [
+ {
+ name: '[CSS_PROP]',
+ value: '[VALUE]',
+ duration: [DURATION_MS],
+ delay: [DELAY_MS],
+ easing: '[EASING_FUNCTION]'
+ },
+ // ... more properties
+ ]
}
]
}
```
-**Variables**:
+### Variables
-- `[CSS_PROPERTY_N]`: CSS property name (e.g., 'background-color', 'color', 'border-radius')
-- `[VALUE_N]`: CSS property value (e.g., '#2563eb', 'white', '12px')
-- Other variables same as previous rules
+- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1.
+- `[TRANSITION_METHOD]` — `StateParams.method`. One of:
+ - `'toggle'` — applies the style state, removes it on next click. Default.
+ - `'add'` — applies the style state. Does not remove on subsequent clicks.
+ - `'remove'` — removes a previously applied style state.
+ - `'clear'` — clears all previously applied style states. Useful for resetting multiple stacked style states at once.
+- `[CSS_PROP]` — CSS property name as a string in camelCase format (e.g. `'backgroundColor'`, `'borderRadius'`, `'opacity'`).
+- `[VALUE]` — target CSS value for the property.
+- `[DURATION_MS]` — transition duration in milliseconds.
+- `[DELAY_MS]` — optional transition delay in milliseconds.
+- `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`.
-**Example - Theme Toggle**:
+---
-```typescript
-{
- key: 'theme-switcher',
- trigger: 'click',
- params: {
- method: 'toggle'
- },
- effects: [
- {
- key: 'page-body',
- transition: {
- duration: 400,
- easing: 'ease-in-out',
- styleProperties: [
- { name: 'background-color', value: '#1a1a1a' },
- { name: 'color', value: '#ffffff' },
- { name: 'border-color', value: '#374151' },
- { name: '--accent-color', value: '#475137ff' } // custom CSS properties are also supported
- ]
- },
- effectId: 'theme-switch'
- }
- ]
-}
-```
+## Rule 3: customEffect with PointerTriggerParams
-**Example - Button Style Toggle**:
+Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation, randomized behavior). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline.
```typescript
{
- key: 'style-toggle',
+ key: '[SOURCE_KEY]',
trigger: 'click',
params: {
- method: 'toggle'
+ type: '[EVENT_TRIGGER_TYPE]'
},
effects: [
{
- key: 'style-toggle',
- transition: {
- duration: 300,
- easing: 'ease-out',
- styleProperties: [
- { name: 'background-color', value: '#ef4444' },
- { name: 'color', value: '#ffffff' },
- { name: 'border-radius', value: '24px' },
- { name: 'transform', value: 'scale(1.05)' }
- ]
- }
+ key: '[TARGET_KEY]',
+ customEffect: [CUSTOM_EFFECT_CALLBACK],
+ duration: [DURATION_MS],
+ easing: '[EASING_FUNCTION]'
}
]
}
```
-**Example - Card State Toggle**:
+### Variables
-```typescript
-{
- key: 'interactive-card',
- trigger: 'click',
- params: {
- method: 'toggle'
- },
- effects: [
- {
- key: 'interactive-card',
- transition: {
- duration: 250,
- easing: 'ease-in-out',
- styleProperties: [
- { name: 'background-color', value: '#f3f4f6' },
- { name: 'border-color', value: '#2563eb' },
- { name: 'box-shadow', value: '0 20px 25px rgba(0,0,0,0.15)' }
- ]
- }
- }
- ]
-}
-```
+- `[SOURCE_KEY]` / `[TARGET_KEY]` / `[EVENT_TRIGGER_TYPE]` — same as Rule 1.
+- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with `progress` from 0 to 1.
+- `[DURATION_MS]` — animation duration in milliseconds.
+- `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`.
---
-## Rule 5: Click with Sequence (Staggered Multi-Element Orchestration)
+## Rule 4: Sequences
-**Use Case**: Click-triggered coordinated animations across multiple elements with staggered timing (e.g., page section reveals, multi-element toggles, orchestrated content entrances)
-
-**When to Apply**:
-
-- When a click should animate multiple elements with staggered timing
-- For orchestrated content reveals (heading, body, image in sequence)
-- When you want easing-controlled stagger instead of manual delays
-- For toggle-able multi-element sequences
-
-**Pattern**:
+Use sequences when a click should sync/stagger animations across multiple elements.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'click',
params: {
- type: 'alternate'
+ type: '[EVENT_TRIGGER_TYPE]'
},
sequences: [
{
offset: [OFFSET_MS],
offsetEasing: '[OFFSET_EASING]',
effects: [
- { effectId: '[EFFECT_ID_1]', key: '[TARGET_KEY_1]' },
- { effectId: '[EFFECT_ID_2]', key: '[TARGET_KEY_2]' },
- { effectId: '[EFFECT_ID_3]', key: '[TARGET_KEY_3]' }
+ // can be `selector` or `listContainer` for multiple effects, or a separate effect definitions with
+ {
+ // can be an inline Effect, or a reference to an effect defined in top level `effects` map
+ effectId: '[EFFECT_ID]',
+ listContainer: '[LIST_CONTAINER_SELECTOR]'
+ }
]
}
]
}
```
-**Variables**:
-
-- `[OFFSET_MS]`: Stagger offset in ms between consecutive effects (typically 100-200ms)
-- `[OFFSET_EASING]`: Easing for stagger distribution — `'linear'`, `'quadIn'`, `'sineOut'`, etc.
-- `[EFFECT_ID_N]`: Effect id from the effects registry for each element
-- `[TARGET_KEY_N]`: Element key for each target
-- Other variables same as Rule 1
-
-**Example - Orchestrated Content Reveal**:
-
-```typescript
-{
- key: 'reveal-button',
- trigger: 'click',
- params: {
- type: 'alternate'
- },
- sequences: [
- {
- offset: 150,
- offsetEasing: 'sineOut',
- effects: [
- { effectId: 'heading-entrance', key: 'content-heading' },
- { effectId: 'body-entrance', key: 'content-body' },
- { effectId: 'image-entrance', key: 'content-image' }
- ]
- }
- ]
-}
-```
+Each `[EFFECT_ID]` must be defined in the top-level `effects` map of the `InteractConfig`:
```typescript
effects: {
- 'heading-entrance': {
- key: 'content-heading',
- duration: 600,
- easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
- keyframeEffect: {
- name: 'heading-in',
- keyframes: [
- { transform: 'translateX(-40px)', opacity: 0 },
- { transform: 'translateX(0)', opacity: 1 }
- ]
- },
- fill: 'both'
- },
- 'body-entrance': {
- key: 'content-body',
- duration: 500,
- easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
+ '[EFFECT_ID]': {
+ duration: [DURATION_MS],
+ easing: '[EASING_FUNCTION]',
+ fill: 'both',
+ // keyframeEffect or namedEffect
keyframeEffect: {
- name: 'body-in',
- keyframes: [
- { transform: 'translateY(20px)', opacity: 0 },
- { transform: 'translateY(0)', opacity: 1 }
- ]
- },
- fill: 'both'
- },
- 'image-entrance': {
- key: 'content-image',
- duration: 700,
- easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
- keyframeEffect: {
- name: 'image-in',
- keyframes: [
- { transform: 'scale(0.8) rotate(-5deg)', opacity: 0 },
- { transform: 'scale(1) rotate(0deg)', opacity: 1 }
- ]
- },
- fill: 'both'
- }
-}
-```
-
----
-
-## Advanced Patterns and Combinations
-
-### Multi-Target Click Effects
-
-When one click should animate multiple elements (without stagger, use `effects`; with stagger, prefer `sequences` above):
-
-```typescript
-{
- key: 'master-control',
- trigger: 'click',
- params: {
- type: 'alternate'
- },
- effects: [
- {
- key: 'element-1',
- namedEffect: { type: 'FadeIn' },
- duration: 300,
- delay: 0,
- fill: 'both'
- },
- {
- key: 'element-2',
- namedEffect: { type: 'SlideIn' },
- duration: 400,
- delay: 100,
- fill: 'both'
- },
- {
- key: 'element-3',
- transition: {
- duration: 200,
- delay: 200,
- styleProperties: [
- { name: 'box-shadow', value: '0 20px 25px rgba(0.2,0,0,0.15)' }
- ]
- }
+ name: '[EFFECT_NAME]',
+ keyframes: [KEYFRAMES]
}
- ]
-}
-```
-
-### Click with Animation Chaining
-
-Using effectId for sequential animations:
-
-```typescript
-// First click animation
-{
- key: 'sequence-trigger',
- trigger: 'click',
- params: {
- type: 'once'
- },
- effects: [
- {
- key: 'first-element',
- namedEffect: { type: 'FadeIn' },
- duration: 500,
- effectId: 'first-fade'
- }
- ]
-},
-// Chained animation
-{
- key: 'first-element',
- trigger: 'animationEnd',
- params: {
- effectId: 'first-fade'
- },
- effects: [
- {
- key: 'second-element',
- namedEffect: { type: 'SlideIn' },
- duration: 400,
- effectId: 'second-slide'
- }
- ]
+ }
}
```
----
-
-## Best Practices for Click Interactions
-
-### Timing and Pattern Guidelines
-
-1. **Keep click animations short** (100-500ms) for immediate feedback
-2. **Use alternate pattern** for toggle states
-3. **Use repeat pattern** for confirmation actions
-4. **Use state pattern** for media controls
-
-### Common Use Cases by Pattern
-
-**Alternate Pattern**:
-
-- Navigation menus
-- Accordion sections
-- Modal dialogs
-- Sidebar toggles
-- Dropdown menus
-
-**State Pattern**:
-
-- Video/audio controls
-- Loading animations
-- Slideshow controls
-- Progress indicators
-
-**Repeat Pattern**:
-
-- Action confirmations
-- Notification badges
-- Button feedback
-- Success animations
-- Error indicators
-
-**Transition Effects**:
+### Variables
-- Theme switching
-- Style variations
-- Color changes
-- Simple state toggles
-- CSS custom property updates
+- `[SOURCE_KEY]` / `[EVENT_TRIGGER_TYPE]` — same as Rule 1.
+- `[OFFSET_MS]` — time offset between each child's animation start, in milliseconds.
+- `[OFFSET_EASING]` — easing curve for the stagger distribution.
+- `[EFFECT_ID]` — string key referencing an entry in the top-level `effects` map. Same concept as `[UNIQUE_EFFECT_ID]` in Rule 1.
+- `[LIST_CONTAINER_SELECTOR]` — CSS selector for the container whose direct children will be staggered.
+- Effect definition variables (`[DURATION_MS]`, `[EASING_FUNCTION]`, `[EFFECT_NAME]`, `[KEYFRAMES]`) — same as Rule 1.
diff --git a/packages/interact/rules/full-lean.md b/packages/interact/rules/full-lean.md
index 5e1b3374..3eb06513 100644
--- a/packages/interact/rules/full-lean.md
+++ b/packages/interact/rules/full-lean.md
@@ -1,504 +1,614 @@
-### Basic usage (quick start)
+# @wix/interact — Rules
+
+Declarative configuration-driven interaction library. Binds animations to triggers via JSON config.
+
+## Table of Contents
+
+- [Common Pitfalls](#common-pitfalls)
+- [Quick Start](#quick-start)
+- [Element Binding](#element-binding)
+- [Config Structure](#config-structure)
+- [Interactions](#interactions)
+- [Triggers](#triggers)
+ - [hover / click](#hover--click)
+ - [viewEnter](#viewenter)
+ - [viewProgress](#viewprogress)
+ - [pointerMove](#pointermove)
+ - [animationEnd](#animationend)
+- [Effects](#effects)
+ - [Time-based Effect](#time-based-effect)
+ - [Scroll / Pointer-driven Effect](#scroll--pointer-driven-effect)
+ - [Transition Effect](#transitioneffect-css-style-toggle)
+ - [Animation Payloads](#animation-payloads)
+- [Sequences](#sequences)
+- [Conditions](#conditions)
+- [FOUC Prevention](#fouc-prevention)
+- [Element Resolution](#element-resolution)
+- [Static API](#static-api)
+
+---
+
+## Common Pitfalls
+
+Each item here is CRITICAL — ignoring any of them will break animations.
+
+- **CRITICAL — `overflow: hidden` breaks `viewProgress`** — replace with `overflow: clip` on all ancestors between source and scroll container. In Tailwind, replace `overflow-hidden` with `overflow-clip`.
+- **Perspective**: Prefer `transform: perspective(...)` inside keyframes. Use the CSS `perspective` property only when multiple children share the same `perspective-origin`.
+- **CRITICAL — Do NOT guess preset options**: If you don't know the expected type/structure for a `namedEffect` param, omit it — rely on defaults rather than guessing.
+- **CRITICAL - Hit-area shift**: When a hover effect changes the size or position of the hovered element (e.g., `transform: scale(…)`), MUST use a separate source and target elements. Otherwise the hit-area shifts, causing rapid enter/leave.
+ events and flickering. Use `selector` to target a child element, or set the effect's `key` to a different element.
+- **Reduced motion**: Use conditions to provide gentler alternatives (shorter durations, fewer transforms, no perpetual motion) for users who prefer reduced motion. You can also set `Interact.forceReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches` to force a global reduced-motion behavior programmatically.
+
+---
+
+## Quick Start
+
+```bash
+npm install @wix/interact @wix/motion-presets
+```
-- Import the runtime and create it with a config defining the interactions.
-- Call `Interact.create(config)` once to initialize.
-- Create the full configuration up‑front and pass it in a single `create` call to avoid unintended overrides; subsequent calls replace the previous config.
+Create the full config up-front and pass it in a single `create` call. Subsequent calls create new `Interact` instances. When creating multiple instances, each manages its own set of interactions independently — use separate instances for isolated component scopes or lazy-loaded sections.
-**For web (Custom Elements):**
+**Web (Custom Elements):**
```ts
import { Interact } from '@wix/interact/web';
-import type { InteractConfig } from '@wix/interact';
-
-const config: InteractConfig = {
- // config-props
-};
-
-Interact.create(config);
+const instance = Interact.create(config);
```
-**For React:**
+The `config` object is an `InteractConfig` containing `interactions` (required), and optionally shared `effects`, `sequences`, and `conditions`.
+
+**React:**
```ts
import { Interact } from '@wix/interact/react';
-import type { InteractConfig } from '@wix/interact';
+const instance = Interact.create(config);
+```
-const config: InteractConfig = {
- // config-props
-};
+**Vanilla JS:**
-Interact.create(config);
+```ts
+import { Interact } from '@wix/interact';
+const instance = Interact.create(config);
+instance.add(element, 'hero'); // bind after element exists in DOM
+instance.remove('hero'); // unregister
```
-### Using `namedEffect` presets (`registerEffects`)
+**CDN (no build tools):**
-Before using `namedEffect`, you must register the presets with the `Interact` instance. Without this, `namedEffect` types will not resolve.
+```html
+
+```
+
+**Registering presets** — MUST be called before using calling `Interact.create()` with usage of `namedEffect`:
```ts
-import { Interact } from '@wix/interact/web'; // or /react
import * as presets from '@wix/motion-presets';
-
Interact.registerEffects(presets);
-Interact.create(config);
```
-Or register only what you need:
+Or selectively:
```ts
import { FadeIn, ParallaxScroll } from '@wix/motion-presets';
Interact.registerEffects({ FadeIn, ParallaxScroll });
```
-- Without Node/build tools: add a `
+### Web: ``
+
+- MUST set `data-interact-key` to a unique value.
+- MUST contain at least one child element (the library targets `.firstElementChild`).
+- If an effect targets a different element, that element also needs its own ``.
+
+```html
+
+ ...
+
```
-### Preventing FOUC for entrance animations
+### React: `` component
+
+- MUST set `tagName` to the replaced element's HTML tag.
+- MUST set `interactKey` to a unique string.
+
+```tsx
+import { Interaction } from '@wix/interact/react';
-- Use `generate(config)` to create critical CSS that hides elements until their `viewEnter` entrance animation plays.
-- Add `data-interact-initial="true"` to the `` that should have its first child hidden initially.
-- Only use `data-interact-initial="true"` for `` with `viewEnter` trigger and `type: 'once'`, where the source and target elements are the same.
-- Do NOT use for `hover` or `click` interactions.
+
+ ...
+;
+```
-**Usage:**
+---
-```javascript
-import { generate } from '@wix/interact/web';
+## Config Structure
-const config = {
- /*...*/
+```ts
+type InteractConfig = {
+ interactions: Interaction[]; // REQUIRED
+ effects?: Record; // reusable effects referenced by effectId
+ sequences?: Record; // reusable sequences by sequenceId
+ conditions?: Record; // named conditions; keys are condition ids
};
+```
-// Generate CSS at build time or on server
-const css = generate(config);
+All cross-references (by id) MUST point to existing entries. Element keys MUST be stable for the config's lifetime.
-// Include in your HTML template
-const html = `
-
-
-
-
-
-
-
-
-
Welcome to Our Site
-
This content fades in smoothly without flash
-
-
-
-
-
-`;
+---
+
+## Interactions
+
+Each interaction maps a source element + trigger to one or more effects.
+
+**Multiple effects per interaction:** A single interaction can contain multiple effects in its `effects` array. All effects in the same interaction share the same trigger — they all fire together when the trigger activates. Use this to apply different animations to different targets from the same trigger event, rather than creating separate interactions with duplicate trigger configs.
+
+```ts
+{
+ key: string; // REQUIRED — matches data-interact-key / interactKey
+ trigger: TriggerType; // REQUIRED
+ params?: TriggerParams; // trigger-specific options
+ effects?: (Effect | EffectRef)[]; // possible to add multiple effects for same trigger
+ sequences?: (SequenceConfig | SequenceConfigRef)[]; // possible to add multiple sequences for same trigger
+ conditions?: string[]; // ids referencing the top-level conditions map; all must pass
+ selector?: string; // CSS selector to refine element selection
+ listContainer?: string; // optional — CSS selector for list container
+ listItemSelector?: string; // optional — CSS selector to filter which children of listContainer are selected
+}
```
-### General guidelines (avoiding common pitfalls)
-
-- Missing required fields or invalid references SHOULD be treated as no-ops for the offending interaction/effect while leaving the rest of the config functional.
-- Params with incorrect types or shapes (especially for `namedEffect` preset options) can produce console errors. If you do not know the expected type/structure for a param, omit it and rely on defaults rather than guessing.
-- Using `overflow: hidden` or `overflow: auto` can break viewProgress animations. Prefer `overflow: clip` for clipping semantics while preserving normal ViewTimeline.
-- When animating with perspective, prefer `transform: perspective(...)` inside keyframes/presets. Reserve the static CSS `perspective` property for the specific case where multiple children of the same container must share the same viewpoint (`perspective-origin`).
-- Stacking contexts and `viewProgress` (ViewTimeline): Creating a new stacking context on the target or any of its ancestors can prevent or freeze ViewTimeline sampling in some engines and setups. Avoid stacking‑context‑creating styles on the observed subtree (target and ancestors), including `transform`, `filter`, `perspective`, `opacity < 1`, `mix-blend-mode`, `isolation: isolate`, aggressive `will-change`, and `contain: paint/layout/size`. If needed for visuals, wrap the content and apply these styles to an inner child so the element that owns the timeline remains “flat”. Also avoid turning the scroll container into a stacking context; if you need clipping, prefer `overflow: clip` and avoid `transform` on the container. Typical symptoms are `viewProgress` not running, jumping 0→1, or never reaching anchors—remove or relocate the offending styles.
-
-### InteractConfig – Rules for authoring interactions (AI-agent oriented)
-
-This configuration declares what user/system triggers occur on which source element(s), and which visual effects should be applied to which target element(s). It is composed of three top-level sections: `effects`, `conditions`, and `interactions`.
-
-### Global rules
-
-- **Required/Optional**: You MUST provide an `interactions` array. You SHOULD provide an `effects` registry when you want to reference reusable effects by id. `conditions` and `sequences` are OPTIONAL.
-- **Cross-references**: All cross-references (by id) MUST point to existing entries (e.g., an `EffectRef.effectId` MUST exist in `effects`).
-- **Element keys**: All element keys (`key` fields) refer to the element path string (e.g., the value used in `data-interact-key`) and MUST be stable for the lifetime of the configuration.
-- **List context**: Where both a list container and list item selector are provided, they MUST describe the same list context across an interaction and its effects. Mismatched list contexts will be ignored by the system.
-- **Conditions**: Conditions act as guards. If any condition on an interaction or effect evaluates to false, the corresponding trigger/effect WILL NOT be applied.
-- **Element binding**: Do NOT add observers/listeners manually. For web, wrap the DOM subtree with `` and set `data-interact-key` to the element key. For React, use the `` component with `interactKey` prop. Use the same key in your config (`Interaction.key`/`Effect.key`). The runtime binds triggers/effects via this attribute.
-
-### Structure
-
-- **effects: Record**
- - **Purpose**: A registry of reusable, named effect definitions that can be referenced from interactions via `EffectRef`.
- - **Key (string)**: The effect id. MUST be unique across the registry.
- - **Value (Effect)**: A full effect definition. See Effect rules below.
-
-- **sequences?: Record**
- - **Purpose**: A registry of reusable sequence definitions that can be referenced from interactions via `SequenceConfigRef`.
- - **Key (string)**: The sequence id. MUST be unique across the registry.
- - **Value (SequenceConfig)**: A full sequence definition. See Sequences section below.
-
-- **conditions?: Record**
- - **Purpose**: Named predicates that gate interactions/effects by runtime context.
- - **Key (string)**: The condition id. MUST be unique across the registry.
- - **Value (Condition)**:
- - **type**: `'media' | 'container' | 'selector'`
- - `'media'`: The predicate MUST be a valid CSS media query expression without the outer `@media` keyword (e.g., `'(min-width: 768px)'`).
- - `'container'`: The predicate SHOULD be a valid CSS container query condition string relative to the relevant container context.
- - `'selector'`: The predicate is a CSS selector pattern. If it contains `&`, the `&` is replaced with the base element selector; otherwise the predicate is appended to the base selector. Used for conditional styling (e.g., `:nth-of-type(odd)`, `.active`).
- - **predicate?**: OPTIONAL textual predicate for the given type. If omitted, the condition is treated as always-true (i.e., a no-op guard).
-
-- **interactions: Interaction[]**
- - **Purpose**: Declarative mapping from a source element and trigger to one or more target effects.
- - Each `Interaction` contains:
- - **key: string**
- - REQUIRED. The source element path. The trigger attaches to this element.
- - **listContainer?: string**
- - OPTIONAL. A CSS selector for a list container context. When present, the trigger is scoped to items within this list.
- - **listItemSelector?: string**
- - OPTIONAL. A CSS selector used to select items within `listContainer`.
- - **trigger: TriggerType**
- - REQUIRED. One of:
- - `'hover' | 'click' | 'activate' | 'interest'`: Pointer interactions (`activate` = click with keyboard Space/Enter; `interest` = hover with focus).
- - `'viewEnter' | 'pageVisible' | 'viewProgress'`: Viewport visibility/progress triggers.
- - `'animationEnd'`: Fires when a specific effect completes on the source element.
- - `'pointerMove'`: Continuous pointer motion over an area.
- - **params?: TriggerParams**
- - OPTIONAL. Parameter object that MUST match the trigger:
- - hover/click/activate/interest: `StateParams | PointerTriggerParams` (activate uses same params as click; interest uses same params as hover).
- - `StateParams.method`: `'add' | 'remove' | 'toggle' | 'clear'`
- - `PointerTriggerParams.type?`: `'once' | 'repeat' | 'alternate' | 'state'`
- - Usage:
- - When the effect is a `TransitionEffect`, use `StateParams.method` to control the state toggle invoked on interaction:
- - `'toggle'` (default): Hover — adds on enter and removes on leave. Click — toggles on each click.
- - `'add'`: Apply the state on the event; hover leave will NOT auto‑remove.
- - `'remove'`: Remove the state on the event.
- - `'clear'`: Clear/reset the effect’s state for the element (or list item when list context is used).
- - With lists (`listContainer`/`listItemSelector`), the state is set on the matching item only.
- - When the effect is a time animation (`namedEffect`/`keyframeEffect`), use `PointerTriggerParams.type`:
- - `'alternate'` (default): Hover — play on enter, reverse on leave. Click — alternate play/reverse on successive clicks.
- - `'repeat'`: Restart from progress 0 on each event; on hover leave the animation is canceled.
- - `'once'`: Play once and remove the listener (hover attaches only the enter listener; no leave).
- - `'state'`: Hover — play on enter if idle/paused, pause on leave if running. Click — toggle play/pause on successive clicks until finished.
- - viewEnter/pageVisible/viewProgress: `ViewEnterParams`
- - `type?`: `'once' | 'repeat' | 'alternate' | 'state'`
- - `threshold?`: number in [0,1] describing intersection threshold
- - `inset?`: string CSS-style inset for rootMargin/observer geometry
- - Usage:
- - `'once'`: Play on first visibility and unobserve the element.
- - `'repeat'`: Play each time the element re‑enters visibility according to `threshold`/`inset`.
- - `'alternate'`: Triggers on re‑entries; if you need alternating direction, set it on the effect (e.g., `alternate: true`) rather than relying on the trigger.
- - `'state'`: Play on entry, pause on exit (for looping/continuous animations).
- - `threshold`: Passed to `IntersectionObserver.threshold` — typical values are 0.1–0.6 for entrances.
- - `inset`: Applied as vertical `rootMargin` (`top/bottom`), e.g., `'-100px'` to trigger earlier/later; left/right remain 0.
- - Note: For `viewProgress`, `threshold` and `inset` are ignored; progress is driven by ViewTimeline/scroll scenes. Control the range via `ScrubEffect.rangeStart/rangeEnd` and `namedEffect.range`.
- - animationEnd: `AnimationEndParams`
- - `effectId`: string of the effect to wait for completion
- - Usage: Fire when the specified effect (by `effectId`) on the source element finishes, useful for chaining sequences.
- - pointerMove: `PointerMoveParams`
- - `hitArea?`: `'root' | 'self'` (default `'self'`)
- - `axis?`: `'x' | 'y'` - when using `keyframeEffect` with `pointerMove`, selects which pointer coordinate maps to linear 0-1 progress; defaults to `'y'`. Ignored for `namedEffect` and `customEffect`.
- - Usage:
- - `'self'`: Track pointer within the source element’s bounds.
- - `'root'`: Track pointer anywhere in the viewport (document root).
- - Only use with `ScrubEffect` mouse presets (`namedEffect`) or `customEffect` that consumes pointer progress; avoid `keyframeEffect` with `pointerMove` unless mapping a single axis via `axis`.
- - When using `customEffect` with `pointerMove`, the progress parameter is an object:
- - ```typescript
- type Progress = {
- x: number; // 0-1: horizontal position (0 = left edge, 1 = right edge)
- y: number; // 0-1: vertical position (0 = top edge, 1 = bottom edge)
- v?: {
- // Velocity (optional)
- x: number; // Horizontal velocity
- y: number; // Vertical velocity
- };
- active?: boolean; // Whether mouse is currently in the hit area
- };
- ```
-
- - **conditions?: string[]**
- - OPTIONAL. Array of condition ids that MUST all pass for this trigger to be active.
- - **selector?: string**
- - OPTIONAL. Additional CSS selector to refine element selection:
- - Without `listContainer`: Uses `querySelectorAll` to match all elements within the root element as separate items.
- - With `listContainer`: Uses `querySelectorAll` within the container to find matching elements as list items. For dynamically added list items, uses `querySelector` within each item to find a single matching element.
- - **effects?: Array**
- - The effects to apply when the trigger fires. Ordering is significant: the first array entry is applied first. The system may reverse internal storage to preserve this application order.
- - At least one of `effects` or `sequences` MUST be provided.
- - **sequences?: Array**
- - OPTIONAL. Sequences to play when the trigger fires. Each sequence coordinates multiple effects with staggered timing. See Sequences section below.
-
-### Sequences (coordinated multi-effect stagger)
-
-Sequences let you group multiple effects into a single coordinated timeline with staggered delays. Instead of manually setting `delay` on each effect, you define `offset` (ms between items) and `offsetEasing` (how that offset is distributed).
-
-**Prefer sequences over manual delay stagger** for any multi-element entrance or orchestration pattern.
-
-- **SequenceConfig** type:
- - `effects: (Effect | EffectRef)[]` — REQUIRED. The effects in this sequence, applied in array order.
- - `delay?: number` — Base delay (ms) before the entire sequence starts. Default `0`.
- - `offset?: number` — Stagger offset (ms) between consecutive effects. Default `0`.
- - `offsetEasing?: string | ((p: number) => number)` — Easing function for stagger distribution. Named easings: `'linear'`, `'quadIn'`, `'quadOut'`, `'sineOut'`, `'cubicIn'`, `'cubicOut'`, `'cubicInOut'`. Also accepts `'cubic-bezier(...)'` strings or a JS function `(p: number) => number`. Default `'linear'`.
- - `sequenceId?: string` — Id for caching and referencing. Auto-generated if omitted.
- - `conditions?: string[]` — Condition ids that MUST all pass for this sequence to be active.
-
-- **SequenceConfigRef** type (referencing a reusable sequence):
- - `sequenceId: string` — REQUIRED. MUST match a key in `InteractConfig.sequences`.
- - `delay?`, `offset?`, `offsetEasing?`, `conditions?` — OPTIONAL overrides merged on top of the referenced sequence.
-
-- Effects within a sequence follow the same rules as standalone effects. Each effect can:
- - Target a different element via `key` (cross-element sequences).
- - Use `listContainer` to target list children (each child becomes a separate effect in the sequence).
- - Reference the effects registry via `effectId`.
-
-- A sequence is treated as a single animation unit by the trigger handler—it plays, reverses, and alternates as one.
-
-**Example — viewEnter staggered list using `listContainer`**:
-
-```typescript
+At least one of `effects` or `sequences` MUST be provided.
+
+For most use cases, `key` alone is sufficient for both source and target resolution. The `selector`, `listContainer`, and `listItemSelector` fields are only needed for advanced patterns (lists, delegated triggers, child targeting). See [Element Resolution](#element-resolution) for details.
+
+---
+
+## Triggers
+
+| Trigger | Description | Accessible variant |
+| :------------- | :------------------------------ | :------------------------------------------ |
+| `hover` | Mouse enter/leave | `interest` (hover + focusin/out) |
+| `click` | Mouse click | `activate` (click + keydown on Enter/Space) |
+| `viewEnter` | Element enters viewport | — |
+| `viewProgress` | Scroll-driven (ViewTimeline) | — |
+| `pointerMove` | Continuous pointer motion | — |
+| `animationEnd` | Fires after an effect completes | — |
+
+### hover / click
+
+Use `type` (via `PointerTriggerParams`) for keyframe/named effects, `method` (via `StateParams`) for transitions. Do NOT use both `type` and `method` together.
+
+**PointerTriggerParams** (`type`):
+
+| Type | hover behavior | click behavior |
+| :---------------------- | :-------------------------------------- | :------------------------------- |
+| `'alternate'` (default) | Play on enter, reverse on leave | Alternate play/reverse per click |
+| `'repeat'` | Play on enter, stop and rewind on leave | Restart per click |
+| `'once'` | Play once on first enter only | Play once on first click only |
+| `'state'` | Play on enter, pause on leave | Toggle play/pause per click |
+
+**StateParams** (`method`) — for `TransitionEffect`:
+
+| Method | hover behavior | click behavior |
+| :------------------- | :---------------------------------------------- | :--------------------------- |
+| `'toggle'` (default) | Add style state on enter, remove on leave | Toggle style state per click |
+| `'add'` | Add style state on enter; leave does NOT remove | Add style state on click |
+| `'remove'` | Remove style state on enter | Remove style state on click |
+| `'clear'` | Clear/reset all style states on enter | Clear/reset all style states |
+
+### viewEnter
+
+```ts
+params: {
+ type: 'once' | 'repeat' | 'alternate' | 'state';
+ threshold?: number; // 0–1, IntersectionObserver threshold
+ inset?: string; // like view-timeline-inset, e.g. '-100px' or '-50px 0px'
+}
+```
+
+**CRITICAL:** When source and target are the **same element**, MUST use `type: 'once'`. For `repeat` / `alternate` / `state`, ALWAYS use **separate** source and target elements — animating the observed element can cause it to leave/re-enter the viewport, causing rapid re-triggers.
+
+### viewProgress
+
+Scroll-driven animations using native `ViewTimeline`. Progress is driven by scroll position. Control the range via `rangeStart`/`rangeEnd` on the effect (see [Scroll / Pointer-driven Effect](#scroll--pointer-driven-effect)).
+
+`viewProgress` has no trigger params. Range configuration (`rangeStart`/`rangeEnd`) is on the effect, not on the trigger.
+
+**CRITICAL:** Replace ALL `overflow: hidden` with `overflow: clip` on every element between the trigger source and the scroll container. `overflow: hidden` creates a new scroll context that breaks ViewTimeline. In Tailwind replace `overflow-hidden` with `overflow-clip`.
+
+### pointerMove
+
+```ts
+params: {
+ hitArea?: 'self' | 'root'; // 'self' = source element bounds, 'root' = viewport
+ axis?: 'x' | 'y'; // restricts tracking to a single axis (for keyframeEffect)
+}
+```
+
+**Rules:**
+
+- Source element MUST NOT have `pointer-events: none`.
+- MUST NOT use the same element as both source and target with size or position effects — use `selector` to target a child or set a different `key`.
+- Use a `(hover: hover)` media condition to disable on touch-only devices. On touch-only devices prefer `viewEnter` or `viewProgress` fallbacks.
+- For 2D effects, use `namedEffect` mouse presets or `customEffect`. `keyframeEffect` only supports a single axis.
+- For independent 2-axis control with keyframes, use two separate interactions (one `axis: 'x'`, one `axis: 'y'`) with `composite: 'add'` or `'accumulate'` on the second effect.
+
+**`centeredToTarget`** — set `true` when source and target are different elements, or when using `hitArea: 'root'` with a specific target, so the coordinate origin is centered on the target.
+
+**Progress object** (for `customEffect`):
+
+```ts
+{ x: number; y: number; v?: { x: number; y: number }; active?: boolean }
+// x, y: 0–1 normalized position; v: velocity; active: whether in active range
+```
+
+### animationEnd
+
+```ts
+params: {
+ effectId: string;
+} // the effect to wait for
+```
+
+Fires when the specified effect completes on the source element. Useful for chaining sequences.
+
+---
+
+## Effects
+
+Each effect applies a visual change to a target element. An effect is either inline or referenced by `effectId` from the top-level `effects` registry. See [Element Resolution](#element-resolution) for how the target is determined.
+
+### Common fields
+
+```ts
+{
+ key?: string; // target element key; omit to target the source
+ effectId?: string; // reference to effects registry (EffectRef)
+ conditions?: string[]; // ids referencing the top-level conditions map; all must pass
+ selector?: string; // CSS selector to refine target
+ listContainer?: string;
+ listItemSelector?: string; // optional — filter which children of listContainer are selected
+ composite?: 'replace' | 'add' | 'accumulate';
+ fill?: 'none' | 'forwards' | 'backwards' | 'both';
+}
+```
+
+**`fill` guidance:**
+
+- `'both'` — use for scroll-driven (`viewProgress`), pointer-driven (`pointerMove`), and toggling effects (`hover`/`click` with `alternate`, `repeat`, or `state` type).
+- `'backwards'` — use for entrance animations with `type: 'once'` when the element's own CSS already matches the final keyframe (applies the initial keyframe during any `delay`).
+
+**`composite`** — controls how this effect combines with others on the same property (transforms & filters):
+
+- `'replace'` (default): fully replaces prior values.
+- `'add'`: concatenates transform/filter functions after any existing ones (e.g. existing `translateX(10px)` + added `translateY(20px)` → both apply).
+- `'accumulate'`: merges arguments of matching functions (e.g. `translateX(10px)` + `translateX(20px)` → `translateX(30px)`); non-matching functions concatenate like `'add'`.
+
+### Time-based Effect
+
+Used with `hover`, `click`, `viewEnter`, `animationEnd` triggers.
+
+```ts
+{
+ duration: number; // REQUIRED (ms)
+ easing?: string; // CSS easing or named easing (see below)
+ delay?: number; // ms
+ iterations?: number; // >=1 or Infinity; 0 is treated as Infinity
+ alternate?: boolean;
+ reversed?: boolean;
+ fill?: 'none' | 'forwards' | 'backwards' | 'both';
+ composite?: 'replace' | 'add' | 'accumulate';
+ // + exactly one animation payload (see below)
+}
+```
+
+**Named easings** from `@wix/motion` (in addition to standard CSS easings):
+
+`'linear'`, `'ease'`, `'ease-in'`, `'ease-out'`, `'ease-in-out'`, `'sineIn'`, `'sineOut'`, `'sineInOut'`, `'quadIn'`, `'quadOut'`, `'quadInOut'`, `'cubicIn'`, `'cubicOut'`, `'cubicInOut'`, `'quartIn'`, `'quartOut'`, `'quartInOut'`, `'quintIn'`, `'quintOut'`, `'quintInOut'`, `'expoIn'`, `'expoOut'`, `'expoInOut'`, `'circIn'`, `'circOut'`, `'circInOut'`, `'backIn'`, `'backOut'`, `'backInOut'`, or any `'cubic-bezier(...)'` / `'linear(...)'` string.
+
+### Scroll / Pointer-driven Effect
+
+Used with `viewProgress` and `pointerMove` triggers.
+
+```ts
+{
+ rangeStart?: RangeOffset; // REQUIRED for viewProgress
+ rangeEnd?: RangeOffset; // REQUIRED for viewProgress
+ easing?: string; // CSS easing or named easing (see above)
+ iterations?: number; // NOT Infinity
+ alternate?: boolean;
+ reversed?: boolean;
+ fill?: 'none' | 'forwards' | 'backwards' | 'both';
+ composite?: 'replace' | 'add' | 'accumulate';
+ centeredToTarget?: boolean;
+ transitionDuration?: number; // ms, smoothing on progress jumps (primarily for pointerMove)
+ transitionDelay?: number; // ms (primarily for pointerMove)
+ transitionEasing?: 'linear' | 'hardBackOut' | 'easeOut' | 'elastic' | 'bounce';
+ // + exactly one animation payload (see below)
+}
+```
+
+**RangeOffset**:
+
+```ts
+{
+ name?: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing';
+ offset?: { value: number; unit: 'percentage' | 'px' | 'vh' | 'vw' }
+}
+```
+
+| Range name | Meaning |
+| :--------- | :--------------------------------------------------- |
+| `entry` | Element entering viewport |
+| `exit` | Element exiting viewport |
+| `contain` | Element fully within view |
+| `cover` | Full range from `entry` through `contain` and `exit` |
+
+**Sticky container pattern** — for scroll-driven animations inside a stuck `position: sticky` container:
+
+- Tall wrapper: height defines scroll distance (e.g. `300vh` for ~2 viewport-heights of scroll travel).
+- Sticky child (`key`) with `position: sticky; top: 0; height: 100vh`: stays fixed while the wrapper scrolls. This is the ViewTimeline source.
+- Use `rangeStart/rangeEnd` with `name: 'contain'` to animate only during the stuck phase.
+
+### TransitionEffect (CSS style toggle)
+
+Used with `hover` / `click` triggers. Pair with `StateParams` (`method`).
+
+```ts
+// Shared timing for all properties:
+{
+ transition: {
+ duration?: number; delay?: number; easing?: string;
+ styleProperties: [{ name: string; value: string }]
+ }
+}
+
+// Per-property timing:
+{
+ transitionProperties: [
+ { name: string; value: string; duration?: number; delay?: number; easing?: string }
+ ]
+}
+```
+
+CSS property names use **camelCase** (e.g. `'backgroundColor'`, `'borderRadius'`).
+
+### Animation Payloads
+
+Exactly one MUST be provided per time-based or scroll/pointer-driven effect:
+
+1. **`namedEffect`** (preferred) — pre-built presets from `@wix/motion-presets`. GPU-friendly and tuned.
+
+ ```ts
+ namedEffect: {
+ type: ('[PRESET_NAME]', [PRESET_OPTIONS]);
+ }
+ ```
+
+ - `[PRESET_NAME]` — one of the registered preset names (see table below).
+ - `[PRESET_OPTIONS]` — optional preset-specific properties. **CRITICAL:** Do NOT guess option names/types. Omit unknown options and rely on defaults.
+
+ Available presets:
+
+ | Category | Presets |
+ | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+ | Entrance | `FadeIn`, `GlideIn`, `SlideIn`, `FloatIn`, `RevealIn`, `ExpandIn`, `BlurIn`, `FlipIn`, `ArcIn`, `ShuttersIn`, `CurveIn`, `DropIn`, `FoldIn`, `ShapeIn`, `TiltIn`, `WinkIn`, `SpinIn`, `TurnIn`, `BounceIn` |
+ | Ongoing | `Pulse`, `Spin`, `Breathe`, `Bounce`, `Wiggle`, `Flash`, `Flip`, `Fold`, `Jello`, `Poke`, `Rubber`, `Swing`, `Cross` |
+ | Scroll | `FadeScroll`, `RevealScroll`, `ParallaxScroll`, `MoveScroll`, `SlideScroll`, `GrowScroll`, `ShrinkScroll`, `TiltScroll`, `PanScroll`, `BlurScroll`, `FlipScroll`, `SpinScroll`, `ArcScroll`, `ShapeScroll`, `ShuttersScroll`, `SkewPanScroll`, `Spin3dScroll`, `StretchScroll`, `TurnScroll` |
+ | Mouse | `TrackMouse`, `Tilt3DMouse`, `Track3DMouse`, `SwivelMouse`, `AiryMouse`, `ScaleMouse`, `BlurMouse`, `SkewMouse`, `BlobMouse` |
+ - **CRITICAL** — Scroll presets (`*Scroll`) used with `viewProgress` MUST include `range` in options: `'in'` (ends at idle state), `'out'` (starts from idle state), or `'continuous'` (passes through idle). Prefer `'continuous'`.
+ - Mouse presets are preferred over `keyframeEffect` for `pointerMove` 2D effects.
+
+2. **`keyframeEffect`** — custom keyframe animations.
+
+ ```ts
+ keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [KEYFRAMES] }
+ ```
+
+ - `[EFFECT_NAME]` — unique string identifier for this effect.
+ - `[KEYFRAMES]` — array of keyframe objects using standard WAAPI format (e.g. `[{ opacity: '0' }, { opacity: '1' }]`). Property names in camelCase.
+
+3. **`customEffect`** — imperative update callback. Use only when CSS-based effects cannot express the desired behavior (e.g., animating SVG attributes, canvas, text content).
+
+ ```ts
+ customEffect: [CUSTOM_EFFECT_CALLBACK];
+ ```
+
+ - `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: Element, progress: number | ProgressObject) => void`. Called on each animation frame.
+
+---
+
+## Sequences
+
+Coordinate multiple effects with staggered timing. Prefer sequences over manual delay stagger.
+
+### Sequence As type
+
+```ts
+{
+ effects: (Effect | EffectRef)[]; // REQUIRED
+ delay?: number; // ms before sequence starts
+ offset?: number; // ms between each child's animation start
+ offsetEasing?: string; // easing curve for staggering offsets
+ sequenceId?: string; // for caching/referencing
+ conditions?: string[]; // ids referencing the top-level conditions map
+}
+```
+
+### Template
+
+```ts
{
interactions: [
{
- key: 'card-grid',
- trigger: 'viewEnter',
- params: { type: 'once', threshold: 0.3 },
+ key: '[SOURCE_KEY]',
+ trigger: '[TRIGGER]',
+ params: [TRIGGER_PARAMS],
sequences: [
{
- offset: 100,
- offsetEasing: 'quadIn',
+ offset: [OFFSET_MS],
+ offsetEasing: '[OFFSET_EASING]',
+ delay: [DELAY_MS],
effects: [
+ // if used `listContainer` each item in the list is a target of a child effect
{
- effectId: 'card-entrance',
- listContainer: '.card-grid',
+ effectId: '[EFFECT_ID]',
+ listContainer: '[LIST_CONTAINER_SELECTOR]',
},
+ // if multiple effects are given each generated effect is added to the sequence
],
},
],
},
],
effects: {
- 'card-entrance': {
- // ...
+ '[EFFECT_ID]': {
+ // effect definition (namedEffect, keyframeEffect, or customEffect)
},
},
}
```
-### Working with elements
+### Variables
-#### Web: `` custom element
+- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for /vanilla, `interactKey` for React).
+- `[TRIGGER]` — any timeb-based trigger type (e.g., `'viewEnter'`, `'activate'`, `'interest'`).
+- `[TRIGGER_PARAMS]` — trigger-specific parameters (e.g., `{ type: 'once', threshold: 0.3 }`).
+- `[OFFSET_MS]` — ms between each child's animation start.
+- `[OFFSET_EASING]` — easing curve for staggering offsets. One of: `'linear'`, `'quadIn'`, `'quadOut'`, `'sineOut'`, `'cubicIn'`, `'cubicOut'`, `'cubicInOut'`, `'cubic-bezier(...)'`, or `'linear(...)'`.
+- `[DELAY_MS]` — optional. Base delay (ms) before the entire sequence starts.
+- `[EFFECT_ID]` — string key referencing an entry in the top-level `effects` map.
+- `[LIST_CONTAINER_SELECTOR]` — CSS selector for the container whose children will be staggered.
-- Wrap the interactive DOM subtree with the custom element and set `data-interact-key` to a stable key. Reference that same key from your config via `Interaction.key` (and optionally `Effect.key`). No observers/listeners or manual DOM querying are needed—the runtime binds triggers and effects via this attribute.
-- If an effect targets an element that is not the interaction's source, you MUST also wrap that target element's subtree with its own `` and set `data-interact-key` to the target's key (the value used in `Effect.key` or the referenced registry Effect's `key`). This is required so the runtime can locate and apply effects to non-source targets.
-- MUST have a `data-interact-key` attribute with a value that is unique within the scope.
-- MUST contain at least one child element.
+Reusable sequences can be defined in `InteractConfig.sequences` and referenced by `sequenceId`.
-```html
-
-
-
+---
+
+## Conditions
+
+Named conditions that gate interactions, effects, or sequences.
+
+| Type | Predicate |
+| :--------- | :------------------------------------------------------------------------ |
+| `media` | CSS media query condition without `@media` (e.g., `'(min-width: 768px)'`) |
+| `selector` | CSS selector; `&` is replaced with the base element selector |
+
+Attach via `conditions: ['[CONDITION_ID]']` on interactions, effects, or sequences. On an interaction, conditions gate the entire trigger; on an effect, only that specific effect is skipped. All listed conditions must pass.
+
+### Examples
+
+```ts
+conditions: {
+ 'desktop': { type: 'media', predicate: '(min-width: 768px)' },
+ 'hover-device': { type: 'media', predicate: '(hover: hover)' },
+ 'reduced-motion': { type: 'media', predicate: '(prefers-reduced-motion: reduce)' },
+ 'odd-items': { type: 'selector', predicate: ':nth-of-type(odd)' },
+}
```
+---
+
+## FOUC Prevention
+
+**Problem:** Elements with entrance animations (e.g. `viewEnter` + `type: 'once'` with `FadeIn`) start in their final visible state. Before the animation framework initializes and applies the starting keyframe (e.g. `opacity: 0`), the element is briefly visible at full opacity — causing a flash of unstyled/un-animated content (FOUC).
+
+**Solution:** Two things are required — both MUST be present:
+
+1. **Generate critical CSS** using `generate(config)` — produces CSS rules that hide entrance-animated elements from the moment the page renders.
+2. **Mark elements with `initial`** — tells the runtime which elements have critical CSS applied so it can coordinate with the generated styles.
+
+### Step 1: Generate CSS
+
+Call `generate(config)` server-side or at build time and inject the result into the `` (preferred), or insert to beginning of ``, so it loads before the page content is painted:
+
```ts
-import type { InteractConfig } from '@wix/interact';
+import { generate } from '@wix/interact/web';
+const css = generate(config);
+```
-const config: InteractConfig = {
- interactions: [
- {
- key: 'my-button', // matches data-interact-key
- trigger: 'hover',
- effects: [
- {
- // key omitted -> targets the source element ('my-button')
- // effect props go here (e.g., transition | keyframeEffect | namedEffect | customEffect)
- },
- ],
- },
- ],
-};
+**Append to `` or beginning of ``:**
+
+```html
+
```
-For a different target element:
+### Step 2: Mark elements
+
+**Web (Custom Elements):**
```html
-
-
+
+ ...
+```
-
- Badge
-
+**React:**
+
+```tsx
+
+ ...
+
```
-```ts
-const config: InteractConfig = {
- interactions: [
- {
- key: 'my-button',
- trigger: 'click',
- effects: [
- {
- key: 'my-badge', // target is different than source
- // effect props (e.g., transition | keyframeEffect | namedEffect)
- },
- ],
- },
- ],
-};
+**Vanilla:**
+
+```html
+...
```
-#### React: `` component
+### Rules
-- MUST replace the element itself with the `` component.
-- MUST set the `tagName` prop with the tag of the replaced element.
-- MUST set the `interactKey` prop to a unique string within the scope.
+- `generate()` should be called server-side or at build time. Can also be called on client-side if page content is initially hidden (e.g. behind a loader/splash screen).
+- **Both** `generate(config)` CSS **and** `initial` on the element are required. Using only one has no effect.
+- Only valid for `viewEnter` + `type: 'once'` where source and target are the same element.
+- For `repeat`/`alternate`/`state`, do NOT use `generate()`/`initial`. Instead, manually apply the initial keyframe as inline styles on the target element and use `fill: 'both'`.
-```tsx
-import { Interaction } from '@wix/interact/react';
+---
-function MyComponent() {
- return (
-
- Click me
-
- );
-}
-```
+## Element Resolution
-For a different target element:
+For simple use cases, `key` on the interaction matches the element, and the same element is both trigger source and animation target. The fields below are only needed for advanced patterns (lists, delegated triggers, child targeting).
-```tsx
-import { Interaction } from '@wix/interact/react';
+### Source element resolution (Interaction)
-function MyComponent() {
- return (
- <>
-
- Click me
-
-
-
- Badge
-
- >
- );
-}
-```
+The source element is what the trigger attaches to. Resolved in priority order:
+
+1. **`listContainer` + `listItemSelector`** — trigger attaches to each element matching `listItemSelector` within the `listContainer`. Use `listItemSelector` only when you need to **filter** which children participate (e.g. select only `.active` items). If all immediate children should participate, omit `listItemSelector`.
+2. **`listContainer` only** — trigger attaches to each immediate child of the container. This is the common case for lists.
+3. **`listContainer` + `selector`** — trigger attaches to the element found via `querySelector` within each immediate child of the container.
+4. **`selector` only** — trigger attaches to all elements matching `querySelectorAll` within the root ``.
+5. **Fallback** — first child of `` (web) or the root element (react/vanilla).
+
+### Target element resolution (Effect)
+
+The target element is what the effect animates. Resolved in priority order:
+
+1. **`Effect.key`** — the `` with matching `data-interact-key`.
+2. **Registry Effect's `key`** — if the effect is an `EffectRef`, the `key` from the referenced registry entry is used.
+3. **Fallback to `Interaction.key`** — the source element acts as the target.
+4. After resolving the root target, `selector`, `listContainer`, and `listItemSelector` on the effect further refine which child elements within that target are animated (same priority order as source resolution).
+
+---
+
+## Static API
+
+| Method / Property | Description |
+| :---------------------------------- | :----------------------------------------------------------------------------------------------------------------- |
+| `Interact.create(config)` | Initialize with a config. Returns the instance. Store the instance to manage its lifecycle. |
+| `Interact.registerEffects(presets)` | Register named effect presets. MUST be called before `create`. |
+| `Interact.destroy()` | Tear down all instances. Call on unmount or route change to prevent memory leaks. |
+| `Interact.forceReducedMotion` | `boolean` (default: `false`) — force reduced-motion behavior regardless of OS setting. |
+| `Interact.allowA11yTriggers` | `boolean` (default: `false`) — enable accessibility trigger variants (`interest`, `activate`). |
+| `Interact.setup(options)` | Configure global thresholds and observer options for scroll, pointer, and viewEnter systems. Call before `create`. |
-The config remains the same for both integrations—only the HTML/JSX setup differs.
-
-### Effect rules (applies to both registry-defined Effect and inline Effect within interactions)
-
-- Common fields (`EffectBase`):
- - **key?: string**
- - OPTIONAL. Target element path. If omitted, resolution follows TARGET CASCADE:
- 1. `Effect.key` (if provided)
- 2. If Effect is an `EffectRef`: lookup registry by `effectId` and use that registry Effect’s `key`
- 3. Fallback to the `Interaction.key` (i.e., source acts as target)
- - **listContainer?: string, listItemSelector?: string**
- - OPTIONAL. If provided, MUST match the interaction's list context when both exist.
- - **conditions?: string[]**
- - OPTIONAL. All conditions MUST pass for the effect to run (in addition to interaction conditions).
- - **selector?: string**
- - OPTIONAL. Additional CSS selector to refine target element selection:
- - Without `listContainer`: Uses `querySelectorAll` to match all elements within the target root as separate items.
- - With `listContainer`: Uses `querySelectorAll` within the container to find matching elements as list items. For dynamically added list items, uses `querySelector` within each item to find a single matching element.
- - **effectId?: string**
- - For `EffectRef` this field is REQUIRED and MUST reference an entry in `effects`.
-
-- Composition and fill usage
- - **composite** (similar to CSS `animation-composition` / WAAPI `composite`):
- - Controls how this effect combines with other effects targeting the same element/property.
- - `'replace'` (default): this effect fully replaces prior values for overlapping properties.
- - `'add'`: this effect adds to the underlying value where the property supports additive composition (e.g., transforms, filters, opacity).
- - `'accumulate'`: values build up across iterations/repeats where supported.
- - Note: If a property is not additive, the runtime will treat `'add'`/`'accumulate'` like `'replace'`.
- - **fill** (like CSS `animation-fill-mode`):
- - `'none'`: styles are only applied while the effect is actively running/in-range.
- - `'forwards'`: the effect’s end state is retained after completion (or last sampled value for scrub).
- - `'backwards'`: the start state applies before the effect begins (or before `rangeStart` for scrub/during `delay` for time effects).
- - `'both'`: combines `'backwards'` and `'forwards'`.
- - For scroll-driven animations (`viewProgress`), prefer `fill: 'both'` to preserve start/end states around the active range and avoid flicker on rapid scroll.
-
-- Types of `Effect` (exactly one MUST be provided via discriminated fields):
- 1. **TimeEffect** (discrete animation over time)
- - `duration`: number (REQUIRED)
- - `easing?`: string (CSS/WAAPI easing)
- - `iterations?`: number (>=1 or Infinity)
- - `alternate?`: boolean (direction alternation)
- - `fill?`: `'none' | 'forwards' | 'backwards' | 'both'`
- - `composite?`: `'replace' | 'add' | 'accumulate'`
- - `reversed?`: boolean
- - `delay?`: number (ms)
- - One of:
- - `keyframeEffect`: `{ name: string; keyframes: Keyframe[] }`
- - `namedEffect`: `NamedEffect` (from `@wix/motion-presets`)
- - `customEffect`: `(element: Element, progress: any) => void`
-
- 2. **ScrubEffect** (animation driven by scroll/progress)
- - `easing?`: string
- - `iterations?`: number (NOT Infinity)
- - `alternate?`: boolean
- - `fill?`: `'none' | 'forwards' | 'backwards' | 'both'`
- - `composite?`: `'replace' | 'add' | 'accumulate'`
- - `reversed?`: boolean
- - `rangeStart`: `RangeOffset`
- - `rangeEnd`: `RangeOffset`
- - `centeredToTarget?`: boolean // If `true` centers the coordinate range at the target element, otherwise uses source element
- - `transitionDuration?`: number (ms for smoothing on progress jumps)
- - `transitionDelay?`: number
- - `transitionEasing?`: `ScrubTransitionEasing`
- - One of `keyframeEffect | namedEffect | customEffect` (see above)
- - For mouse-effects driven by the `pointerMove` trigger, avoid `keyframeEffect` unless using `params: { axis: 'x' | 'y' }` to map a single pointer axis to linear 0–1 progress. For 2D effects, use `namedEffect` mouse presets or `customEffect` instead.
- - For scroll `namedEffect` presets (e.g., `*Scroll`) used with a `viewProgress` trigger, include `range: 'in' | 'out' | 'continuous'` in the `namedEffect` options; prefer `'continuous'` for simplicity.
- - RangeOffset (used by `rangeStart`/`rangeEnd`):
- - Type: `{ name: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing'; offset: LengthPercentage }`
- - name?: Optional logical anchor derived from ViewTimeline concepts.
- - 'entry': Leading edge of the target crosses into the view/container.
- - 'exit': Trailing edge of the target crosses out of the view/container.
- - 'contain': Interval where the target is fully within the view/container.
- - 'cover': Interval where the view/container is fully covered by the target.
- - 'entry-crossing': The moment the target's center crosses the entry boundary.
- - 'exit-crossing': The moment the target's center crosses the exit boundary.
- - If omitted, the runtime chooses a context-appropriate anchor; specify explicitly for deterministic behavior.
- - offset: A `LengthPercentage` that shifts the anchor boundary.
- - Explicit format: `{ value: number; unit: 'percentage' | 'px' | 'em' | 'rem' | 'vh' | 'vw' | 'vmin' | 'vmax' }`
- - Percentages are interpreted along the relevant scroll axis relative to the observation area (e.g., viewport or container size).
- - Positive values move the anchor "forward" along the scroll direction; negative values move it "backward".
- - Examples:
- - Start when the element is 20% inside the viewport: `rangeStart: { name: 'entry', offset: { value: 20, unit: 'percentage' } }`
- - End when the element is leaving: `rangeEnd: { name: 'exit', offset: { value: 0, unit: 'percentage' } }`
-
- 3. **TransitionEffect** (CSS transition-style state toggles)
- - `key?`: string (target override; see TARGET CASCADE)
- - `effectId?`: string (when used as a reference identity)
- - One of:
- - `transition?`: `{ duration?: number; delay?: number; easing?: string; styleProperties: { name: string; value: string }[] }`
- - Applies a single transition options block to all listed style properties.
- - `transitionProperties?`: `Array<{ name: string; value: string; duration?: number; delay?: number; easing?: string }>`
- - Allows per-property transition options. If both `transition` and `transitionProperties` are provided, the system SHOULD apply both with per-property entries taking precedence for overlapping properties.
-
-### Authoring rules for animation payloads (`namedEffect`, `keyframeEffect`, `customEffect`):
-
-- **namedEffect (Preferred)**: Use first for best performance. These are pre-built presets from `@wix/motion-presets` that are GPU-friendly and tuned.
- - Structure: `namedEffect: { type: '', /* optional preset options like direction (bottom|top|left|right), etc. do not use those without having proper documentation of which options exist and of what types. */ }`
- - Short list of common preset names:
- - Entrance: `FadeIn`, `BounceIn`, `SlideIn`, `FlipIn`, `ArcIn`
- - Ongoing: `Pulse`, `Spin`, `Wiggle`, `Bounce`
- - Scroll: `ParallaxScroll`, `FadeScroll`, `RevealScroll`, `TiltScroll` — for `viewProgress`, `namedEffect` options MUST include `range: 'in' | 'out' | 'continuous'`; prefer `'continuous'`
- - Mouse: `TrackMouse`, `Tilt3DMouse`, `ScaleMouse`, `BlurMouse` — for `pointerMove`; prefer over `keyframeEffect` for 2D pointer effects
-- **keyframeEffect (Default for custom animations)**: Prefer this when you need a custom-made animation.
- - Structure: `keyframeEffect: { name: string; keyframes: Keyframe[] }` (keyframes use standard CSS/WAAPI properties).
- - When used with `pointerMove`, requires `params: { axis: 'x' | 'y' }` to select which pointer coordinate maps to linear progress. Without `axis`, pointer progress is two-dimensional and cannot drive keyframe animations. For 2D pointer effects, use `namedEffect` or `customEffect`.
-- **customEffect (Last resort)**: Use only when you must perform DOM manipulation or produce randomized/non-deterministic visuals that cannot be expressed as keyframes or presets.
- - Structure: `customEffect: (element: Element, progress: any) => void`
-
-### Target resolution and list context
-
-- When applying an effect, the system resolves the final target as:
- `Effect.key -> registry Effect.key (for EffectRef) -> Interaction.key`.
-- If a `listContainer` is present on the interaction, the selector resolution may be widened to include list items (optionally filtered by `listItemSelector`), and then further refined by any provided `selector`.
-
-### Reduced motion
-
-- The runtime MAY force reduced motion globally. Authors SHOULD keep effects resilient to reduced motion by avoiding reliance on specific durations or continuous motion.
-- Use `conditions` to provide responsive and accessible behavior:
- - Define media conditions such as `'(prefers-reduced-motion: reduce)'` and breakpoint queries, and attach them to interactions/effects to disable, simplify, or swap animations when appropriate.
- - Provide alternative reduced‑motion variants (e.g., shorter durations, fewer transforms, no perpetual motion/parallax/3D), and select them via `conditions` or effect references so that users who prefer reduced motion get a gentler experience.
+Each `Interact.create()` call returns an instance. Store instances and call `instance.destroy()` when no longer needed (e.g. on component unmount) to prevent stale listeners and memory leaks.
diff --git a/packages/interact/rules/hover.md b/packages/interact/rules/hover.md
index 8eb0cbd6..899e9286 100644
--- a/packages/interact/rules/hover.md
+++ b/packages/interact/rules/hover.md
@@ -1,507 +1,185 @@
# Hover Trigger Rules for @wix/interact
-This document contains rules for generating hover trigger interactions in `@wix/interact`. These rules cover all hover behavior patterns and common use cases.
+This document contains rules for generating hover-triggered interactions in `@wix/interact`.
-## Rule 1: Basic Hover Effect Configuration
+**CRITICAL — Accessible hover**: Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus.
-**Purpose**: Generate basic hover interactions with enter/leave animations
+**CRITICAL — Hit-area shift**: To avoid flickering, use a **separate `[SOURCE_KEY]` and `[TARGET_KEY]`** when the effect changes size or position:
-**Pattern**:
+- `[SOURCE_KEY]` (interaction `key`) — a stable wrapper element that receives the mouse events.
+- `[TARGET_KEY]` (effect `key` or `selector`) — the inner element that actually animates.
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'hover',
- effects: [
- {
- key: '[TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
- fill: 'both',
- duration: [DURATION_MS],
- easing: '[EASING_FUNCTION]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[SOURCE_KEY]`: Unique identifier for hoverable element. Should equal the value of the `data-interact-key` attribute on the wrapping ``.
-- `[TARGET_KEY]`: Unique identifier for animated element (can be same as `[SOURCE_KEY]` for self-targeting, or different for cross-targeting).
-- `[EFFECT_TYPE]`: Either `namedEffect` or `keyframeEffect`
-- `[EFFECT_DEFINITION]`: Named effect object (e.g., { type: 'SlideIn', ...params }, { type: 'FadeIn', ...params }) or keyframe object (e.g., { name: 'custom-fade', keyframes: [{ opacity: 0 }, { opacity: 1 }] }, { name: 'custom-slide', keyframes: [{ transform: 'translateX(-100%)' }, { transform: 'translateX(0)' }] })
-- `[DURATION_MS]`: Animation duration in milliseconds (typically 200-500ms for micro-interactions)
-- `[EASING_FUNCTION]`: Timing function ('ease-out', 'ease-in-out', or cubic-bezier)
-- `[UNIQUE_EFFECT_ID]`: Optional unique identifier for animation chaining
+## Table of Contents
-**Default Values**:
+- [Rule 1: keyframeEffect / namedEffect with PointerTriggerParams](#rule-1-keyframeeffect--namedeffect-with-pointertriggerparams)
+- [Rule 2: transition / transitionProperties with StateParams](#rule-2-transition--transitionproperties-with-stateparams)
+- [Rule 3: customEffect with PointerTriggerParams](#rule-3-customeffect-with-pointertriggerparams)
+- [Rule 4: Sequences](#rule-4-sequences)
-- `DURATION_MS`: 300 (for micro-interactions)
-- `EASING_FUNCTION`: 'ease-out' (for smooth feel)
-- `[TARGET_KEY]`: Same as `[SOURCE_KEY]` for self-targeting
-
-**Common Use Cases**:
-
-- Button hover states
-- Card lift effects
-- Image zoom effects
-- Color/opacity changes
-
-**Example Generations**:
-
-```typescript
-// Button hover
-{
- key: 'primary-button',
- trigger: 'hover',
- effects: [
- {
- key: 'primary-button',
- keyframeEffect: {
- name: 'button-shadow',
- keyframes: [
- { transform: 'scale(1)', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' },
- { transform: 'scale(1.05)', boxShadow: '0 8px 16px rgba(0,0,0,0.15)' }
- ]
- },
- fill: 'both',
- duration: 200,
- easing: 'ease-out'
- }
- ]
-}
+---
-// Image hover zoom
-{
- key: 'product-image',
- trigger: 'hover',
- effects: [
- {
- key: 'product-image-media',
- keyframeEffect: {
- name: 'image-scale',
- keyframes: [
- { transform: 'scale(1)' },
- { transform: 'scale(1.1)' }
- ]
- },
- fill: 'both',
- duration: 400,
- easing: 'ease-out'
- }
- ]
-}
-```
+## Rule 1: keyframeEffect / namedEffect with PointerTriggerParams
-## Rule 2: Hover Alternate Animations (namedEffect / keyframeEffect)
+Use `keyframeEffect` or `namedEffect` when the hover should play an animation (CSS or WAAPI). Pair with `PointerTriggerParams` to control playback behavior.
-**Purpose**: Hover interactions that play forward on mouse enter and reverse on mouse leave (`type: 'alternate'`).
+**CRITICAL:** Always include `fill: 'both'` for `type: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `type: 'once'`, use `fill: 'backwards'` or `fill: 'none'`.
-**Pattern**:
+**Multiple effects:** The `effects` array can contain multiple effects — all share the same hover trigger and fire together. Use this to animate different targets from a single hover event.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'hover',
params: {
- type: 'alternate'
+ type: '[EVENT_TRIGGER_TYPE]'
},
effects: [
{
key: '[TARGET_KEY]',
- // Use namedEffect OR keyframeEffect:
- namedEffect: { type: '[NAMED_EFFECT_TYPE]' },
- // keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [{ ... }, { ... }] },
- fill: 'both',
- reversed: [REVERSED_BOOL],
- duration: [DURATION_MS],
- easing: '[EASING_FUNCTION]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[REVERSED_BOOL]`: Optional. `true` to reverse the enter direction (mouse enter plays backwards, leave plays forwards).
-- `[NAMED_EFFECT_TYPE]`: Pre-built effect from `@wix/motion-presets`. Available hover presets:
- - Size: `ExpandIn`, `Pulse`, `GrowIn`
- - Fade/Blur: `FadeIn`, `Flash`, `BlurIn`
- - Translate: `SlideIn`, `GlideIn`, `FloatIn`, `BounceIn`, `GlitchIn`
- - Rotate: `SpinIn`, `TiltIn`, `ArcIn`, `TurnIn`, `FlipIn`, `Spin`, `Swing`
- - Attention: `Bounce`, `DropIn`, `Rubber`, `Jello`, `Cross`, `Wiggle`, `Poke`
-- Other variables same as Rule 1
-
-**Important**: Spatial effects (translation, rotation) that change the hit-area considerably should use different source and target keys to avoid flickering on enter/leave.
-
-**Default Values**:
-
-- `DURATION_MS`: 250–300
-- `EASING_FUNCTION`: 'ease-out'
-
-**Example — namedEffect (card scale)**:
-
-```typescript
-{
- key: 'feature-card',
- trigger: 'hover',
- params: { type: 'alternate' },
- effects: [
- {
- key: 'feature-card',
- namedEffect: { type: 'Pulse' },
- fill: 'both',
- duration: 250,
- easing: 'ease-out'
- }
- ]
-}
-```
-
-**Example — keyframeEffect (card lift)**:
-```typescript
-{
- key: 'portfolio-item',
- trigger: 'hover',
- params: { type: 'alternate' },
- effects: [
- {
- key: 'portfolio-item',
+ // --- pick ONE of the two effect types ---
keyframeEffect: {
- name: 'portfolio-lift',
- keyframes: [
- { transform: 'translateY(0)', boxShadow: '0 4px 6px rgba(0,0,0,0.1)' },
- { transform: 'translateY(-8px)', boxShadow: '0 20px 25px rgba(0,0,0,0.15)' }
- ]
+ name: '[EFFECT_NAME]',
+ keyframes: [KEYFRAMES],
},
- fill: 'both',
- duration: 300,
- easing: 'ease-out'
- }
- ]
-}
-```
-
-## Rule 3: Hover Interactions with Repeat Pattern
-
-**Purpose**: Generate hover interactions that restart animation each time mouse enters
-
-**Pattern**:
+ // OR
+ namedEffect: [NAMED_EFFECT_DEFINITION],
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'hover',
- params: {
- type: 'repeat'
- },
- effects: [
- {
- key: '[TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
+ fill: '[FILL_MODE]',
duration: [DURATION_MS],
- easing: '[EASING_FUNCTION]'
- }
+ easing: '[EASING_FUNCTION]',
+ delay: [DELAY_MS],
+ iterations: [ITERATIONS],
+ alternate: [ALTERNATE_BOOL]
+ },
+ // additional effects targeting other elements can be added here
]
}
```
-**Variables**:
-
-- Same as Rule 1
-
-**Use Cases for Repeat Pattern**:
-
-- Attention-grabbing animations
-- Pulse effects
-- Shake/wiggle animations
-- Bounce effects
+### Variables
-**Default Values**:
+- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). The element that listens for hover.
+- `[TARGET_KEY]` — identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).
+- `[EVENT_TRIGGER_TYPE]` — `PointerTriggerParams.type`. One of:
+ - `'alternate'` — plays forward on enter, reverses on leave. Default. Most common for hover.
+ - `'repeat'` — restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.
+ - `'once'` — plays once on the first enter and never again.
+ - `'state'` — resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`).
+- `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.
+- `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`.
+- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.
+- `[DURATION_MS]` — animation duration in milliseconds.
+- `[EASING_FUNCTION]` — CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.
+- `[DELAY_MS]` — optional delay before the effect starts, in milliseconds.
+- `[ITERATIONS]` — optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `type: 'state'`.
+- `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration (within a single playback).
+- `[FILL_MODE]` — usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.
-- `type`: 'repeat'
-- `DURATION_MS`: 600 (longer for noticeable repeat)
-- `EASING_FUNCTION`: 'ease-in-out'
-
-**Example Generations**:
-
-```typescript
-// Button pulse effect
-{
- key: 'cta-button',
- trigger: 'hover',
- params: {
- type: 'repeat'
- },
- effects: [
- {
- key: 'cta-button',
- namedEffect: {
- type: 'Breath'
- },
- duration: 600,
- easing: 'ease-in-out'
- }
- ]
-}
-
-// Icon shake effect
-{
- key: 'notification-bell',
- trigger: 'hover',
- params: {
- type: 'repeat'
- },
- effects: [
- {
- key: 'notification-bell',
- keyframeEffect: {
- name: 'shake',
- keyframes: [
- { transform: 'rotate(0deg)' },
- { transform: 'rotate(15deg)' },
- { transform: 'rotate(-15deg)' },
- { transform: 'rotate(0deg)' }
- ]
- },
- duration: 500,
- easing: 'ease-in-out'
- }
- ]
-}
-```
+---
-## Rule 4: Hover Interactions with Play/Pause Pattern
+## Rule 2: transition / transitionProperties with StateParams
-**Purpose**: Generate hover interactions that pause/resume on hover (state-based control)
+Use `transition` or `transitionProperties` when the hover should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Pair with `StateParams` to control how the style is applied.
-**Pattern**:
+Use `transition` when all properties share timing. Use `transitionProperties` when each property needs independent `duration`, `delay`, or `easing`.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'hover',
params: {
- type: 'state'
+ method: '[TRANSITION_METHOD]'
},
effects: [
{
key: '[TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
- duration: [DURATION_MS],
- iterations: Infinity,
- easing: '[EASING_FUNCTION]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- Same as Rule 1
-
-**Use Cases for State Pattern**:
-
-- Controlling loop animations
-- Pausing video effects
-- Interactive loading spinners
-- Continuous animation control
-
-**Default Values**:
-
-- `type`: 'state'
-- `iterations`: Infinity
-- `DURATION_MS`: 2000 (longer for smooth loops)
-- `EASING_FUNCTION`: 'linear' (for continuous motion)
-**Example Generations**:
-
-```typescript
-// Rotating loader that plays on hover and pauses on mouse leave
-{
- key: 'loading-spinner',
- trigger: 'hover',
- params: {
- type: 'state'
- },
- effects: [
- {
- key: 'loading-spinner',
- keyframeEffect: {
- name: 'spin',
- keyframes: [
- { transform: 'rotate(0deg)' },
- { transform: 'rotate(360deg)' }
+ // --- pick ONE of the two transition forms ---
+ transition: {
+ duration: [DURATION_MS],
+ delay: [DELAY_MS],
+ easing: '[EASING_FUNCTION]',
+ styleProperties: [
+ { name: '[CSS_PROP]', value: '[VALUE]' },
+ // ... more properties
]
},
- duration: 2000,
- iterations: Infinity,
- easing: 'linear'
- }
- ]
-}
-
-// Pulsing element that plays on hover and pauses on mouse leave
-{
- key: 'live-indicator',
- trigger: 'hover',
- params: {
- type: 'state'
- },
- effects: [
- {
- key: 'live-indicator',
- namedEffect: {
- type: 'Pulse'
- },
- duration: 1500,
- iterations: Infinity,
- easing: 'ease-in-out'
- }
- ]
-}
-```
-
-## Rule 5: Multi-Target Hover Effects
-
-**Purpose**: Generate hover interactions that affect multiple elements from a single source
-
-**Pattern**:
-
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'hover',
- params: {
- type: '[BEHAVIOR_TYPE]'
- },
- effects: [
- {
- key: '[TARGET_1]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION_1],
- fill: [FILL_1],
- reversed: [REVERSED_BOOL_1],
- duration: [DURATION_1],
- delay: [DELAY_1]
+ // OR (when each property needs its own timing)
+ transitionProperties: [
+ {
+ name: '[CSS_PROP]',
+ value: '[VALUE]',
+ duration: [DURATION_MS],
+ delay: [DELAY_MS],
+ easing: '[EASING_FUNCTION]'
+ },
+ // ... more properties
+ ]
},
- {
- key: '[TARGET_2]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION_2],
- fill: [FILL_2],
- reversed: [REVERSED_BOOL_2],
- duration: [DURATION_2],
- delay: [DELAY_2]
- }
+ // additional effects targeting other elements can be added here
]
}
```
-**Variables**:
-
-- `[BEHAVIOR_TYPE]`: type of behavior for the effect. use `alternate`, `repeat`, or `state` according to the previous rules.
-- `[FILL_N]`: Optional fill value for the Nth effect - same as CSS animation-fill-mode (e.g. 'both', 'forwards', 'backwards').
-- `[REVERSED_BOOL_N]`: Same as `[REVERSED_BOOL]` from Rule 2 only for the Nth effect.
-- `[DURATION_N]`: Same as `[DURATION_MS]` from Rule 1 only for the Nth effect.
-- `[DELAY_N]`: Delay in milliseconds of the Nth effect.
+### Variables
-**Use Cases**:
+- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1.
+- `[TRANSITION_METHOD]` — `StateParams.method`. One of:
+ - `'toggle'` — applies the style state on enter, removes on leave. Default.
+ - `'add'` — applies the style state on enter. Leave does NOT remove it.
+ - `'remove'` — removes a previously applied style state on enter.
+ - `'clear'` — clears all previously applied style states on enter. Useful for resetting multiple stacked style state changes at once.
+- `[CSS_PROP]` — CSS property name as a string in camelCase format (e.g. `'backgroundColor'`, `'borderRadius'`, `'opacity'`).
+- `[VALUE]` — target CSS value for the property.
+- `[DURATION_MS]` — transition duration in milliseconds.
+- `[DELAY_MS]` — optional transition delay in milliseconds.
+- `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`.
-- Card hover affecting image, text, and button
-- Navigation item hover affecting icon and text
-- Complex component state changes
-
-**Timing Strategies**:
+---
-- Simultaneous: All delays = 0
-- Staggered: Incrementing delays (0, 50, 100ms)
-- Sequential: Non-overlapping delays
+## Rule 3: customEffect with PointerTriggerParams
-**Example Generations**:
+Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline.
```typescript
-// Product card with multiple targets
{
- key: 'product-card',
+ key: '[SOURCE_KEY]',
trigger: 'hover',
params: {
- type: 'alternate'
+ type: '[EVENT_TRIGGER_TYPE]'
},
effects: [
{
- key: 'product-card',
- keyframeEffect: {
- name: 'product-card-move',
- keyframes: [
- { transform: 'translateY(0)' },
- { transform: 'translateY(-8px)' }
- ]
- },
- fill: 'both',
- duration: 200,
- delay: 0
- },
- {
- key: 'product-image',
- keyframeEffect: {
- name: 'product-image-scale',
- keyframes: [
- { transform: 'scale(1)' },
- { transform: 'scale(1.05)' }
- ]
- },
- fill: 'both',
- duration: 300,
- delay: 50
- },
- {
- key: 'product-title',
- keyframeEffect: {
- name: 'product-title-color',
- keyframes: [
- { color: '#374151' },
- { color: '#2563eb' }
- ]
- },
- fill: 'both',
- duration: 150,
- delay: 100
+ key: '[TARGET_KEY]',
+ customEffect: [CUSTOM_EFFECT_CALLBACK],
+ duration: [DURATION_MS],
+ easing: '[EASING_FUNCTION]'
},
- {
- key: 'add-to-cart-btn',
- keyframeEffect: {
- name: 'button-fade',
- keyframes: [
- { opacity: '0', transform: 'translateY(10px)' },
- { opacity: '1', transform: 'translateY(0)' }
- ]
- },
- fill: 'both',
- duration: 200,
- delay: 150
- }
+ // additional effects targeting other elements can be added here
]
}
```
-## Rule 6: Hover with Sequence (Staggered Multi-Target)
+### Variables
-**Purpose**: Hover interactions that stagger animations across multiple targets using a sequence instead of manual delays.
+- `[SOURCE_KEY]` / `[TARGET_KEY]` / `[EVENT_TRIGGER_TYPE]` — same as Rule 1.
+- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with `progress` from 0 to 1.
+- `[DURATION_MS]` — animation duration in milliseconds.
+- `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`.
-**When to Apply**:
+---
-- When hovering a container should stagger-animate its children
-- For list item hover effects with coordinated timing
-- When you want easing-controlled stagger on hover
+## Rule 4: Sequences
-**Pattern**:
+Use sequences when a hover should sync/stagger animations across multiple elements.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'hover',
params: {
- type: 'repeat'
+ type: '[EVENT_TRIGGER_TYPE]'
},
sequences: [
{
@@ -509,6 +187,7 @@ This document contains rules for generating hover trigger interactions in `@wix/
offsetEasing: '[OFFSET_EASING]',
effects: [
{
+ // can be an inline Effect, or a reference to an effect defined in top level `effects` map
effectId: '[EFFECT_ID]',
listContainer: '[LIST_CONTAINER_SELECTOR]'
}
@@ -518,97 +197,28 @@ This document contains rules for generating hover trigger interactions in `@wix/
}
```
-**Example - Hover Card Grid Stagger**:
-
-```typescript
-{
- key: 'card-grid',
- trigger: 'hover',
- params: { type: 'repeat' },
- sequences: [
- {
- offset: 80,
- offsetEasing: 'sineOut',
- effects: [
- {
- effectId: 'item-pop',
- listContainer: '.card-grid-items'
- }
- ]
- }
- ]
-}
-```
+Each `[EFFECT_ID]` must be defined in the top-level `effects` map of the `InteractConfig`:
```typescript
effects: {
- 'item-pop': {
- duration: 400,
- easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
+ '[EFFECT_ID]': {
+ duration: [DURATION_MS],
+ easing: '[EASING_FUNCTION]',
+ fill: '[FILL_MODE]',
+ // keyframeEffect or namedEffect
keyframeEffect: {
- name: 'item-pop',
- keyframes: [
- { transform: 'translateY(16px) scale(0.95)', opacity: 0 },
- { transform: 'translateY(0) scale(1)', opacity: 1 }
- ]
+ name: '[EFFECT_NAME]',
+ keyframes: [KEYFRAMES]
}
}
}
```
----
-
-## Best Practices for Hover Rules
-
-### Timing and Pattern Guidelines
-
-1. **Keep durations short** (100-400ms) for responsiveness
-
-### User Experience Guidelines
-
-1. **Use 'alternate' type** for most hover effects (natural enter/leave)
-2. **Use 'repeat' sparingly** - can be annoying if overused
-3. **Use 'state' for controlling** ongoing animations
-4. **Stagger multi-target effects** for more polished feel
-
-### Timing Recommendations
-
-- **Micro-interactions**: 100-200ms
-- **Button hovers**: 200-300ms
-- **Card/image effects**: 300-400ms
-- **Complex multi-target**: 200-500ms total
-
-### Easing Recommendations
-
-- **Enter animations**: 'ease-out' (quick start, slow end)
-- **Interactive elements**: 'ease-in-out' (smooth both ways)
-- **Attention effects**: 'ease-in-out' (natural feel)
-- **Continuous motion**: 'linear' (consistent speed)
-
-## Accessibility
-
-Use `@wix/interact`'s `conditions` API to skip hover animations for users who prefer reduced motion. Define a `prefers-motion` condition and reference it on any interaction that should be suppressed:
-
-```typescript
-{
- conditions: {
- 'prefers-motion': { type: 'media', predicate: '(prefers-reduced-motion: no-preference)' }
- },
- interactions: [
- {
- key: 'card',
- trigger: 'hover',
- conditions: ['prefers-motion'], // skipped when reduced-motion is preferred
- effects: [/* ... */]
- }
- ]
-}
-```
-
-For pointer-primary devices only, also consider adding a `hover-capable` condition:
-
-```typescript
-'hover-capable': { type: 'media', predicate: '(hover: hover)' }
-```
+### Variables
-Use `trigger: 'interest'` instead of `'hover'` to also handle keyboard focus, which is the accessible equivalent of hover.
+- `[SOURCE_KEY]` / `[EVENT_TRIGGER_TYPE]` — same as Rule 1.
+- `[OFFSET_MS]` — time offset between each child's animation start, in milliseconds.
+- `[OFFSET_EASING]` — easing curve for the stagger distribution (e.g. `'sineOut'`, `'linear'`).
+- `[EFFECT_ID]` — string key referencing an entry in the top-level `effects` map. Same concept as `[UNIQUE_EFFECT_ID]` in Rule 1.
+- `[LIST_CONTAINER_SELECTOR]` — CSS selector for the container whose direct children will be staggered.
+- Effect definition variables (`[DURATION_MS]`, `[EASING_FUNCTION]`, `[FILL_MODE]`, `[EFFECT_NAME]`, `[KEYFRAMES]`) — same as Rule 1.
diff --git a/packages/interact/rules/integration.md b/packages/interact/rules/integration.md
index 7dd8c2bf..0955f7bd 100644
--- a/packages/interact/rules/integration.md
+++ b/packages/interact/rules/integration.md
@@ -1,367 +1,299 @@
# @wix/interact Integration Rules
-This document outlines the rules and best practices for generating code that integrates `@wix/interact` into a webpage.
+Rules for integrating `@wix/interact` into a webpage — binding animations and effects to user-driven triggers via declarative configuration.
-## 1. Overview
+## Table of Contents
-`@wix/interact` is a library for creating interactive animations and effects triggered by user actions (click, hover, scroll, etc.). It works by binding **Triggers** and **Effects** to specific **Elements**.
+- [Entry Points](#entry-points)
+ - [Web (Custom Elements)](#web-custom-elements)
+ - [React](#react)
+ - [Vanilla JS](#vanilla-js)
+- [Named Effects & registerEffects](#named-effects--registereffects)
+- [Configuration Schema](#configuration-schema)
+ - [InteractConfig](#interactconfig)
+ - [Interaction](#interaction)
+ - [Element Selection](#element-selection)
+- [Triggers](#triggers)
+- [Sequences](#sequences)
+- [Critical CSS (FOUC Prevention)](#critical-css-fouc-prevention)
+- [Static API](#static-api)
-## 2. Integrations
+---
-### `web` :: using Custom Elements
+## Entry Points
-#### 1. Basic Setup
+Install with your package manager:
-**Usage:**
+```bash
+npm install @wix/interact @wix/motion-presets
+```
+
+### Web (Custom Elements)
```typescript
import { Interact } from '@wix/interact/web';
-// Define your interaction configuration
-const config = {
- interactions: [
- // ...
- ],
- effects: {
- // ...
- },
-};
-
-// Initialize the interact instance
-const interact = Interact.create(config);
+Interact.create(config);
```
-#### 2. HTML Setup
-
-**Rules:**
-
-- MUST have a `data-interact-key` attribute with a value that is unique within the scope.
-- MUST contain at least one child element.
+The `config` object contains `interactions` (trigger-effect bindings), and optionally `effects`, `sequences`, and `conditions`. See [Configuration Schema](#configuration-schema) for full details.
-**Usage:**
+Wrap target elements with ``:
```html
-
-
-
This will fade in when it enters the viewport!
+
+ ...
```
-### `react` :: using React
+**Rules:**
-#### 1. Basic Setup
+- MUST set `data-interact-key` to a unique string within the page.
+- MUST contain at least one child element (the library targets `.firstElementChild` by default).
-**Usage:**
+### React
```typescript
import { Interact } from '@wix/interact/react';
-// Define your interaction configuration
-const config = {
- interactions: [
- // ...
- ],
- effects: {
- // ...
- },
-};
+Interact.create(config);
+```
-// Initialize the interact instance
-const interact = Interact.create(config);
+Replace target elements with ``:
+
+```tsx
+import { Interaction } from '@wix/interact/react';
+
+
+ ...
+;
```
-#### 2. HTML Setup
+**Rules:**
+
+- MUST set `tagName` to a valid HTML tag string for the element being replaced.
+- MUST set `interactKey` to a unique string within the page.
+
+### Vanilla JS
+
+```typescript
+import { Interact } from '@wix/interact';
+
+const interact = Interact.create(config);
+interact.add(element, 'hero');
+```
**Rules:**
-- MUST replace the element itself with the `` component.
-- MUST set the `tagName` prop with the tag of the replaced element.
-- MUST set the `interactKey` prop to a unique string within the scope.
+- Call `add(element, key)` after elements exist in the DOM.
+- Call `remove(key)` to unregister all interactions for a key.
-**Usage:**
+---
-```tsx
-import { Interaction } from '@wix/interact/react';
+## Named Effects & registerEffects
-function MyComponent() {
- return (
-
- Hello, animated world!
-
- );
-}
+Register `@wix/motion-presets` before calling `Interact.create` — required for using `namedEffect` in any effect definition:
+
+```typescript
+import { Interact } from '@wix/interact/web';
+import * as presets from '@wix/motion-presets';
+
+Interact.registerEffects(presets);
```
-## 3. Configuration Schema
+Or register selectively:
+
+```typescript
+import { FadeIn, ParallaxScroll } from '@wix/motion-presets';
+Interact.registerEffects({ FadeIn, ParallaxScroll });
+```
-The `InteractConfig` object defines the behavior.
+Then use in effects:
+
+```typescript
+{ namedEffect: { type: 'FadeIn' }, duration: 800, easing: 'ease-out' }
+```
+
+For full effect type syntax (`keyframeEffect`, `namedEffect`, `customEffect`, `transition`/`transitionProperties`), see [full-lean.md](./full-lean.md) and the trigger-specific rule files.
+
+---
+
+## Configuration Schema
+
+### InteractConfig
```typescript
type InteractConfig = {
- interactions: Interaction[]; // Required: Array of interaction definitions
- effects?: Record; // Optional: Reusable named effects
- sequences?: Record; // Optional: Reusable sequence definitions
- conditions?: Record; // Optional: Reusable conditions (media queries)
+ interactions: Interaction[];
+ effects?: Record;
+ sequences?: Record;
+ conditions?: Record;
};
```
-### Interaction Definition
+| Field | Description |
+| :------------- | :---------------------------------------------------------------------- |
+| `interactions` | Required. Array of interaction definitions binding triggers to effects. |
+| `effects?` | Reusable effects referenced by `effectId` from interactions. |
+| `sequences?` | Reusable sequence definitions, referenced by `sequenceId`. |
+| `conditions?` | Named conditions (media/container/selector queries), referenced by ID. |
+
+Each call to `Interact.create(config)` creates a new `Interact` instance. A single config can define multiple interactions.
+
+### Interaction
```typescript
{
- key: 'element-key', // Matches data-interact-key
- trigger: 'trigger-type', // e.g., 'hover', 'click'
- selector?: '.child-cls', // Optional: CSS selector to refine target within the element
- listContainer?: '.list', // Optional: CSS selector for a list container (enables list context)
- listItemSelector?: '.item', // Optional: CSS selector for items within listContainer
- params?: { ... }, // Trigger-specific parameters
- conditions?: ['cond-id'], // Array of condition IDs
- effects?: [ ... ], // Array of effects to apply
- sequences?: [ ... ] // Array of sequences (coordinated staggered effects)
+ key: string; // REQUIRED — matches data-interact-key / interactKey
+ trigger: TriggerType; // REQUIRED — trigger type
+ params?: TriggerParams; // trigger-specific parameters
+ selector?: string; // CSS selector to refine target within the element
+ listContainer?: string; // CSS selector for a list container
+ listItemSelector?: string; // optional — CSS selector to filter which children of listContainer are selected
+ conditions?: string[]; // array of condition IDs; all must pass
+ effects?: Effect[]; // effects to apply
+ sequences?: SequenceConfig[]; // sequences to apply
}
```
-### Element Selection Hierarchy
+At least one of `effects` or `sequences` MUST be provided.
-1. **`listContainer` + `listItemSelector`**: Selects matching items within the container as list items.
-2. **`listContainer` only**: Targets immediate children of the container as list items.
-3. **`selector` only**: Matches all elements within the root element (using `querySelectorAll`).
-4. **Fallback**: If none are provided, targets the **first child** of `` in `web` or the root element in `react`.
+**Multiple effects per interaction:** A single interaction can contain multiple effects in its `effects` array. All effects share the same trigger — they fire together when the trigger activates. Use this to animate different targets from the same trigger event instead of duplicating interactions.
-## 4. Generating Critical CSS for Entrance Animations
+### Element Selection
-### `generate(config)`
+**Most common**: Omit `selector`/`listContainer`/`listItemSelector` entirely — the element with the matching key is used as both source and target. Use `selector` to target a child element within the keyed element. Use `listContainer` for staggered sequences across list items.
-Generates critical CSS styles that prevent flash-of-unstyled-content (FOUC) for elements with `viewEnter` entrance animations.
+`listItemSelector` is **optional** — only use it when you need to **filter** which children of `listContainer` participate (e.g. select only `.active` items). When omitted, all immediate children of the `listContainer` are selected.
-**Rules:**
+Resolved in order of priority:
-- MUST be called server-side or at build time to generate static CSS.
-- MUST set `data-interact-initial="true"` on the `` whose first child should be hidden until the animation plays.
-- Only valid when: trigger is `viewEnter` + `params.type` is `'once'` + source element and target element are the same.
-- Do NOT use for `hover`, `click`, or `viewEnter` with `repeat`/`alternate`/`state` types.
+1. **`listContainer` + `listItemSelector`** — matches only the elements matching `listItemSelector` within the container (filtering).
+2. **`listContainer` only** — targets all immediate children of the container (common case).
+3. **`listContainer` + `selector`** — matches via `querySelector` within each immediate child of the container.
+4. **`selector` only** — matches via `querySelectorAll` within the root element.
+5. **Fallback** — first child of `` (web) or the root element (react/vanilla).
-**Usage:**
+---
-```javascript
-import { generate } from '@wix/interact/web';
-
-const config = {
- /*...*/
-};
+## Triggers
-// Generate CSS at build time or on server
-const css = generate(config);
-
-// Include in your HTML template
-const html = `
-
-
-
-
-
-
-
-
- ...
-
-
-
-
-
-`;
-```
+| Trigger | Description | Trigger `params` | Rules |
+| :------------- | :------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | :----------------------------------- |
+| `hover` | Mouse enter/leave | `type?`: `'once'` \| `'alternate'` \| `'repeat'` \| `'state'` — or `method?`: `'add'` \| `'remove'` \| `'toggle'` \| `'clear'` | [hover.md](./hover.md) |
+| `click` | Mouse click | Same as `hover` | [click.md](./click.md) |
+| `interest` | Accessible hover (hover + focus) | Same as `hover` | [hover.md](./hover.md) |
+| `activate` | Accessible click (click + Enter/Space) | Same as `click` | [click.md](./click.md) |
+| `viewEnter` | Element enters viewport | `type?`: same values as hover; `threshold?`: 0–1; `inset?`: CSS length as strin for viewport margin | [viewenter.md](./viewenter.md) |
+| `viewProgress` | Scroll-driven (ViewTimeline) | No trigger params. Configure `rangeStart`/`rangeEnd` on the **effect**, not on `params`. | [viewprogress.md](./viewprogress.md) |
+| `pointerMove` | Mouse movement | `hitArea?`: `'self'` \| `'root'`; `axis?`: `'x'` \| `'y'` | [pointermove.md](./pointermove.md) |
+| `animationEnd` | Chain after another effect | `effectId`: ID of the preceding effect | — |
-## 5. Triggers & Behaviors
+For `hover`/`click` (and their accessible variants `interest`/`activate`): use `type` (via `PointerTriggerParams`) for keyframe/named effects, or `method` (via `StateParams`) for transition effects. Do not use both `type` and `method` together.
-| Trigger | Description | Key Parameters | Rules File |
-| :------------- | :---------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | :------------------ |
-| `hover` | Mouse enter/leave | `type`: 'once', 'alternate', 'repeat', 'state' for animations, or `method`: 'add', 'remove', 'toggle', 'clear' for states | `./hover.md` |
-| `click` | Mouse click | `type`: 'once', 'alternate', 'repeat', 'state' for animations, or `method`: 'add', 'remove', 'toggle', 'clear' for states | `./click.md` |
-| `activate` | Accessible click (click + keyboard Space/Enter) | Same as `click` with keyboard support | `./click.md` |
-| `interest` | Accessible hover (hover + focus) | Same as `hover` with focus support | `./hover.md` |
-| `viewEnter` | Element enters viewport | `type`: 'once', 'alternate', 'repeat', 'state'; `threshold` (0-1) | `./viewenter.md` |
-| `viewProgress` | Scroll-driven using ViewTimeline | (No specific params, uses effect ranges) | `./viewprogress.md` |
-| `pointerMove` | Mouse movement | `hitArea`: 'self' (default) or 'root'; `axis`: 'x' or 'y' for keyframeEffect | `./pointermove.md` |
-| `animationEnd` | Chaining animations | `effectId`: ID of the previous effect | -- |
+---
-## 5b. Sequences (Coordinated Stagger)
+## Sequences
-Sequences group multiple effects into a coordinated timeline with staggered timing. Instead of setting `delay` on each effect manually, define `offset` (ms between items) and `offsetEasing` (how offset is distributed).
-
-### Sequence Config
+Sequences coordinate multiple effects with staggered timing.
```typescript
{
- offset: 100, // ms between consecutive effects
- offsetEasing: 'quadIn', // easing for stagger distribution (linear, quadIn, sineOut, etc.)
- delay: 0, // base delay before the sequence starts
- effects: [ // effects in the sequence, applied in order
- { effectId: 'card-entrance', listContainer: '.card-grid' },
+ offset: number, // ms between consecutive items
+ offsetEasing: string, // Any valid easing string for stagger distribution curve
+ delay: number, // ms base delay before the sequence starts
+ effects: [
+ /* ... effect definitions */,
],
}
```
-Effects in a sequence can target different elements via `key`, use `listContainer` to target list children, or reference the effects registry via `effectId`.
-
-Reusable sequences can be defined in `InteractConfig.sequences` and referenced by `sequenceId`:
+Define reusable sequences in `InteractConfig.sequences` and reference by `sequenceId`:
```typescript
{
sequences: {
- 'stagger-entrance': { offset: 80, offsetEasing: 'quadIn', effects: [{ effectId: 'fade-up', listContainer: '.items' }] },
+ 'stagger-fade': {
+ /* ... sequence defintiion */
+ },
},
interactions: [
- { key: 'section', trigger: 'viewEnter', params: { type: 'once' }, sequences: [{ sequenceId: 'stagger-entrance' }] },
+ {
+ key: `'[SOURCE_KEY]'`,
+ trigger: `'[TRIGGER]'`,
+ params: `[TRIGGER_PARAMS]`,
+ sequences: [{ sequenceId: 'stagger-fade' }],
+ },
],
}
```
-## 6. Named Effects & `registerEffects`
+---
-To use `namedEffect` presets from `@wix/motion-presets`, register them before calling `Interact.create`. For full effect type syntax (`keyframeEffect`, `customEffect`, `TransitionEffect`, `ScrubEffect`), see `full-lean.md`.
+## Critical CSS (FOUC Prevention)
-**Install:**
+**Problem:** Elements with entrance animations (e.g. `FadeIn` on `viewEnter`) are initially visible in their final state. Before the animation framework applies the starting keyframe, the content flashes visibly — a flash of un-animated content (FOUC).
-```bash
-> npm install @wix/motion-presets
-```
+**Solution:** Two things are required — both MUST be present:
-**Import and register:**
+1. **Generate critical CSS** with `generate(config)` — produces CSS that hides entrance-animated elements until the animation plays.
+2. **Mark elements with `initial`** — `data-interact-initial="true"` on ``, or `initial={true}` on `` in React.
-```typescript
-import { Interact } from '@wix/interact/web';
-import * as presets from '@wix/motion-presets';
+Using only one of these has no effect — both are required.
-Interact.registerEffects(presets);
-```
+See [viewenter.md](./viewenter.md) for full details.
-**Or register only required presets:**
+**Rules:**
-```typescript
-import { Interact } from '@wix/interact/web';
-import { FadeIn, ParallaxScroll } from '@wix/motion-presets';
+- `generate()` should be called server-side or at build time. Can also be called on the client if page content is initially hidden (e.g. behind a loader).
+- Only valid for `viewEnter` + `type: 'once'` where source and target are the same element.
-Interact.registerEffects({ FadeIn, ParallaxScroll });
+```javascript
+import { generate } from '@wix/interact/web';
+const css = generate(config);
```
-```typescript
-{
- namedEffect: { type: 'FadeIn' },
- duration: 800,
- easing: 'ease-out'
-}
-```
+**Append to `` or beginning of ``:**
-## 7. Examples
+```html
+
+```
-### Basic Hover (Scale)
+**Web:**
-```typescript
-const config = {
- effects: {
- scaleUp: {
- transitionProperties: [
- {
- name: 'transform',
- value: 'scale(1.1)',
- duration: 300,
- delay: 100,
- easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
- },
- ],
- },
- },
- interactions: [
- {
- key: 'btn',
- trigger: 'hover',
- effects: [
- {
- effectId: 'scaleUp',
- },
- ],
- },
- ],
-};
+```html
+
+ ...
+
```
-### Viewport Entrance
+**React:**
-```typescript
-const config = {
- interactions: [
- {
- key: 'hero',
- trigger: 'viewEnter',
- params: { type: 'once', threshold: 0.2 },
- effects: [
- {
- namedEffect: { type: 'FadeIn' },
- duration: 800,
- },
- ],
- },
- ],
-};
+```tsx
+
+ ...
+
```
-### Staggered List Entrance (Sequence)
+**Vanilla:**
-```typescript
-const config = {
- interactions: [
- {
- key: 'card-grid',
- trigger: 'viewEnter',
- params: { type: 'once', threshold: 0.3 },
- sequences: [
- {
- offset: 100,
- offsetEasing: 'quadIn',
- effects: [{ effectId: 'card-entrance', listContainer: '.card-grid' }],
- },
- ],
- },
- ],
- effects: {
- 'card-entrance': {
- duration: 500,
- easing: 'ease-out',
- keyframeEffect: {
- name: 'card-fade-up',
- keyframes: [
- { transform: 'translateY(40px)', opacity: 0 },
- { transform: 'translateY(0)', opacity: 1 },
- ],
- },
- fill: 'both',
- },
- },
-};
+```html
+...
```
-### Interactive Toggle (Click)
+---
-```typescript
-const config = {
- interactions: [
- {
- key: 'menu-btn',
- trigger: 'click',
- params: { type: 'alternate' },
- effects: [
- {
- key: 'menu-content',
- effectId: 'menu-open', // Creates state 'menu-open'
- keyframeEffect: {
- name: 'slide',
- keyframes: [{ transform: 'translateX(-100%)' }, { transform: 'translateX(0)' }],
- },
- duration: 300,
- },
- ],
- },
- ],
-};
-```
+## Static API
+
+Each `Interact.create(config)` call returns an instance. Keep a reference if you need to add/remove elements dynamically (vanilla JS) or to destroy a specific instance. Call `Interact.destroy()` to tear down all instances at once (e.g. on page navigation).
+
+| Method / Property | Description |
+| :---------------------------------- | :------------------------------------------------------------------------------------------- |
+| `Interact.create(config)` | Initialize with a config. Returns the instance. Multiple configs create separate instances. |
+| `Interact.registerEffects(presets)` | Register named effect presets before `create`. Required for `namedEffect` usage. |
+| `Interact.destroy()` | Tear down all instances. |
+| `Interact.forceReducedMotion` | `boolean` — force reduced-motion behavior regardless of OS setting. Default: `false`. |
+| `Interact.allowA11yTriggers` | `boolean` — enable accessibility triggers (`interest`, `activate`). Default: `false`. |
+| `Interact.setup(options)` | Configure global defaults for scroll/pointer/viewEnter trigger params. Call before `create`. |
diff --git a/packages/interact/rules/pointermove.md b/packages/interact/rules/pointermove.md
index 608ea1c9..2ae2d9fc 100644
--- a/packages/interact/rules/pointermove.md
+++ b/packages/interact/rules/pointermove.md
@@ -1,1074 +1,164 @@
# PointerMove Trigger Rules for @wix/interact
-These rules help generate pointer-driven interactions using the `@wix/interact` library. PointerMove triggers create real-time animations that respond to mouse movement over elements, perfect for 3D effects, cursor followers, and interactive cards.
+These rules help generate pointer-driven interactions using `@wix/interact`. PointerMove triggers create real-time animations that respond to mouse movement over elements or the entire viewport.
-## Core Concepts
+## Table of Contents
-### Effect Types for PointerMove
+- [Trigger Source Elements with `hitArea: 'self'`](#trigger-source-elements-with-hitarea-self)
+- [PointerMoveParams](#pointermoveparams)
+- [Progress Object Structure](#progress-object-structure)
+- [Centering with `centeredToTarget`](#centering-with-centeredtotarget)
+- [Device Conditions](#device-conditions)
+- [Rule 1: namedEffect](#rule-1-namedeffect)
+- [Rule 2: keyframeEffect with Single Axis](#rule-2-keyframeeffect-with-single-axis)
+- [Rule 3: Two keyframeEffects with Two Axes and `composite`](#rule-3-two-keyframeeffects-with-two-axes-and-composite)
+- [Rule 4: customEffect](#rule-4-customeffect)
-The `pointerMove` trigger provides 2D progress (x and y coordinates). You can use:
+## Trigger Source Elements with `hitArea: 'self'`
-1. **`namedEffect`** (Preferred): Pre-built mouse presets from `@wix/motion-presets` that handle 2D progress internally
-2. **`customEffect`** (Advanced): Custom function receiving the 2D progress object for full control
-3. **`keyframeEffect`** (Single-axis): The pointer position on a single axis is mapped to linear 0-1 progress for keyframe animations. Use `axis: 'x'` or `axis: 'y'` (defaults to `'y'`)
+When using `hitArea: 'self'`, the source element is the hit area for pointer tracking:
-### Hit Area Configuration (`hitArea`)
+- The source element **MUST NOT** have `pointer-events: none` — it needs to receive pointer events.
+- **MUST AVOID** using the same element as both source and target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation.
-The `hitArea` parameter determines where mouse movement is tracked:
-
-| Value | Behavior | Best For |
-| -------- | ----------------------------------------------------- | ---------------------------------------- |
-| `'self'` | Tracks mouse within the source element's bounds only | Local hover effects, card interactions |
-| `'root'` | Tracks mouse anywhere in the viewport (document root) | Global cursor followers, ambient effects |
-
-### Progress Object Structure (for `customEffect`)
-
-When using `customEffect` with `pointerMove`, the progress parameter is an object:
-
-```typescript
-type Progress = {
- x: number; // 0-1: horizontal position (0 = left edge, 1 = right edge)
- y: number; // 0-1: vertical position (0 = top edge, 1 = bottom edge)
- v?: {
- // Velocity (optional)
- x: number; // Horizontal velocity
- y: number; // Vertical velocity
- };
- active?: boolean; // Whether mouse is currently in the hit area
-};
-```
-
-### Centering with `centeredToTarget`
-
-Controls how the progress range is calculated:
-
-| Value | Behavior | Use When |
-| ------- | -------------------------------------------------- | ---------------------------------------- |
-| `true` | Centers the coordinate range at the target element | Source and target are different elements |
-| `false` | Uses source element bounds for calculations | Cursor followers, global effects |
-
-## Rule 1: Single Element Pointer Effects with 3D Named Effects
-
-**Use Case**: Interactive 3D transformations on individual elements that respond to mouse position (e.g., card tilting, 3D product showcases, interactive buttons)
-
-**When to Apply**:
-
-- For interactive card hover effects
-- When creating 3D product showcases
-- For engaging button interactions
-- When building interactive UI elements that respond to mouse movement
-
-**Pattern**:
-
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'pointerMove',
- params: {
- hitArea: '[HIT_AREA]'
- },
- effects: [
- {
- key: '[TARGET_KEY]',
- namedEffect: {
- type: '[3D_EFFECT_TYPE]',
- [EFFECT_PROPERTIES]
- },
- centeredToTarget: [CENTERED_TO_TARGET],
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[SOURCE_KEY]`: Unique identifier for source element that tracks mouse movement
-- `[TARGET_KEY]`: Unique identifier for target element to animate (can be same as source or different)
-- `[HIT_AREA]`: 'self' (mouse within source element) or 'root' (mouse anywhere in viewport)
-- `[3D_EFFECT_TYPE]`: 'Tilt3DMouse', 'Track3DMouse', 'SwivelMouse'
-- `[EFFECT_PROPERTIES]`: Named effect specific properties (angle, perspective, direction, etc.)
-- `[CENTERED_TO_TARGET]`: true (center range at target) or false (use source element bounds)
-- `[UNIQUE_EFFECT_ID]`: Optional unique identifier
-
-**Example - Interactive Product Card**:
-
-```typescript
-{
- key: 'product-card',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'product-card',
- namedEffect: {
- type: 'Tilt3DMouse',
- angle: 15,
- perspective: 1000
- }
- }
- ]
-}
-```
-
----
-
-## Rule 2: Single Element Pointer Effects with Movement Named Effects
-
-**Use Case**: Cursor-following and position-tracking effects on individual elements (e.g., floating elements, cursor followers, responsive decorations)
-
-**When to Apply**:
-
-- For cursor-following elements
-- When creating floating responsive decorations
-- For interactive element positioning
-- When building mouse-aware UI components
-
-**Pattern**:
-
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'pointerMove',
- params: {
- hitArea: '[HIT_AREA]'
- },
- effects: [
- {
- key: '[TARGET_KEY]',
- namedEffect: {
- type: '[MOVEMENT_EFFECT_TYPE]',
- distance: { value: [DISTANCE_VALUE], unit: '[DISTANCE_UNIT]' },
- axis: '[AXIS_CONSTRAINT]'
- },
- centeredToTarget: [CENTERED_TO_TARGET],
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[MOVEMENT_EFFECT_TYPE]`: 'TrackMouse', 'AiryMouse', 'BounceMouse'
-- `[DISTANCE_VALUE]`: Numeric value for movement distance
-- `[DISTANCE_UNIT]`: 'px', 'percentage', 'vw', 'vh'
-- `[AXIS_CONSTRAINT]`: 'both', 'horizontal', 'vertical'
-- Other variables same as Rule 1
-
-**Example - Cursor Follower Element**:
-
-```typescript
-{
- key: 'cursor-follower',
- trigger: 'pointerMove',
- params: {
- hitArea: 'root'
- },
- effects: [
- {
- namedEffect: {
- type: 'TrackMouse',
- distance: { value: 50, unit: 'percentage' },
- axis: 'both'
- },
- centeredToTarget: false
- }
- ]
-}
-```
-
-**Example - Floating Decoration**:
-
-```typescript
-{
- key: 'hero-section',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'floating-element',
- namedEffect: {
- type: 'AiryMouse',
- distance: { value: 30, unit: 'px' },
- axis: 'both'
- },
- centeredToTarget: true,
- effectId: 'hero-float'
- }
- ]
-}
-```
-
----
-
-## Rule 3: Single Element Pointer Effects with Scale Named Effects
-
-**Use Case**: Dynamic scaling and deformation effects on individual elements based on mouse position (e.g., interactive scaling, organic transformations, blob effects)
-
-**When to Apply**:
-
-- For interactive scaling buttons
-- When creating organic blob-like interactions
-- For dynamic size responsive elements
-- When building creative morphing interfaces
-
-**Pattern**:
-
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'pointerMove',
- params: {
- hitArea: '[HIT_AREA]'
- },
- effects: [
- {
- key: '[TARGET_KEY]',
- namedEffect: {
- type: '[SCALE_EFFECT_TYPE]',
- [SCALE_PROPERTIES]
- },
- centeredToTarget: [CENTERED_TO_TARGET],
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[SCALE_EFFECT_TYPE]`: 'ScaleMouse', 'BlobMouse', 'SkewMouse'
-- `[SCALE_PROPERTIES]`: Effect-specific properties (scale, distance, axis)
-- Other variables same as Rule 1
-
-**Example - Interactive Scale Button**:
-
-```typescript
-{
- key: 'scale-button',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'scale-button',
- namedEffect: {
- type: 'ScaleMouse',
- scale: 1.1,
- distance: { value: 100, unit: 'px' },
- axis: 'both'
- },
- centeredToTarget: true
- }
- ]
-}
-```
-
-**Example - Organic Blob Effect**:
-
-```typescript
-{
- key: 'blob-container',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'blob-shape',
- namedEffect: {
- type: 'BlobMouse',
- intensity: 0.8,
- smoothness: 0.6
- },
- effectId: 'blob-morph'
- }
- ]
-}
-```
-
----
-
-## Rule 4: Single Element Pointer Effects with Visual Named Effects
-
-**Use Case**: Visual effect transformations on individual elements based on mouse position (e.g., motion blur, rotation effects, visual filters)
-
-**When to Apply**:
-
-- For creative visual interfaces
-- When adding motion blur to interactions
-- For rotation-based mouse effects
-- When creating dynamic visual feedback
-
-**Pattern**:
-
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'pointerMove',
- params: {
- hitArea: '[HIT_AREA]'
- },
- effects: [
- {
- key: '[TARGET_KEY]',
- namedEffect: {
- type: '[VISUAL_EFFECT_TYPE]',
- [VISUAL_PROPERTIES]
- },
- centeredToTarget: [CENTERED_TO_TARGET],
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[VISUAL_EFFECT_TYPE]`: 'BlurMouse', 'SpinMouse'
-- `[VISUAL_PROPERTIES]`: Effect-specific properties (blur amount, rotation speed)
-- Other variables same as Rule 1
-
-**Example - Motion Blur Card**:
-
-```typescript
-{
- key: 'motion-card',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'motion-card',
- namedEffect: {
- type: 'BlurMouse',
- blurAmount: 5,
- motionIntensity: 0.7
- },
- centeredToTarget: true
- }
- ]
-}
-```
-
-**Example - Spinning Element**:
-
-```typescript
-{
- key: 'spin-trigger',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'spinning-icon',
- namedEffect: {
- type: 'SpinMouse',
- rotationSpeed: 0.5,
- direction: 'clockwise'
- },
- centeredToTarget: false,
- effectId: 'icon-spin'
- }
- ]
-}
-```
-
----
-
-## Rule 5: Multi-Element Pointer Parallax Effects with Named Effects
-
-**Use Case**: Coordinated pointer-driven animations across multiple elements creating layered parallax effects (e.g., multi-layer backgrounds, depth effects, coordinated element responses)
-
-**When to Apply**:
-
-- For multi-layer background effects
-- When creating depth and parallax interactions
-- For coordinated UI element responses
-- When building immersive pointer-driven experiences
-
-**Pattern**:
-
-```typescript
-{
- key: '[CONTAINER_KEY]',
- trigger: 'pointerMove',
- params: {
- hitArea: '[HIT_AREA]'
- },
- effects: [
- {
- key: '[BACKGROUND_LAYER_KEY]',
- namedEffect: {
- type: '[BACKGROUND_EFFECT_TYPE]',
- distance: { value: [BACKGROUND_DISTANCE], unit: '[DISTANCE_UNIT]' }
- },
- centeredToTarget: [CENTERED_TO_TARGET]
- },
- {
- key: '[MIDGROUND_LAYER_KEY]',
- namedEffect: {
- type: '[MIDGROUND_EFFECT_TYPE]',
- distance: { value: [MIDGROUND_DISTANCE], unit: '[DISTANCE_UNIT]' }
- },
- centeredToTarget: [CENTERED_TO_TARGET]
- },
- {
- key: '[FOREGROUND_LAYER_KEY]',
- namedEffect: {
- type: '[FOREGROUND_EFFECT_TYPE]',
- distance: { value: [FOREGROUND_DISTANCE], unit: '[DISTANCE_UNIT]' }
- },
- centeredToTarget: [CENTERED_TO_TARGET]
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[CONTAINER_KEY]`: Unique identifier for container element tracking mouse
-- `[*_LAYER_KEY]`: Unique identifier for different layer elements
-- `[*_EFFECT_TYPE]`: Named effects for each layer (typically movement effects)
-- `[*_DISTANCE]`: Movement distance for each layer (creating depth)
-- Other variables same as previous rules
-
-**Example - Parallax Card Layers**:
-
-```typescript
-{
- key: 'parallax-card',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'bg-layer',
- namedEffect: {
- type: 'AiryMouse',
- distance: { value: 15, unit: 'px' },
- axis: 'both'
- },
- centeredToTarget: true
- },
- {
- key: 'mid-layer',
- namedEffect: {
- type: 'TrackMouse',
- distance: { value: 25, unit: 'px' },
- axis: 'both'
- },
- centeredToTarget: true
- },
- {
- key: 'fg-layer',
- namedEffect: {
- type: 'BounceMouse',
- distance: { value: 35, unit: 'px' },
- axis: 'both'
- },
- centeredToTarget: true
- }
- ]
-}
-```
-
-**Example - Multi-Layer Hero Section**:
-
-```typescript
-{
- key: 'hero-container',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'hero-bg',
- namedEffect: {
- type: 'AiryMouse',
- distance: { value: 20, unit: 'px' },
- axis: 'both'
- },
- centeredToTarget: true
- },
- {
- key: 'hero-content',
- namedEffect: {
- type: 'TrackMouse',
- distance: { value: 40, unit: 'px' },
- axis: 'horizontal'
- },
- centeredToTarget: true
- },
- {
- key: 'hero-decorations',
- namedEffect: {
- type: 'ScaleMouse',
- scale: 1.05,
- distance: { value: 60, unit: 'px' }
- },
- centeredToTarget: true
- }
- ]
-}
-```
-
----
-
-## Rule 6: Coordinated Group Pointer Effects with Named Effects
-
-**Use Case**: Synchronized pointer-driven animations across related elements with different responses (e.g., card grids, navigation menus, interactive galleries)
-
-**When to Apply**:
-
-- For interactive card grids
-- When building responsive navigation systems
-- For gallery hover effects
-- When creating coordinated interface responses
-
-**Pattern**:
-
-```typescript
-{
- key: '[CONTAINER_KEY]',
- trigger: 'pointerMove',
- params: {
- hitArea: '[HIT_AREA]'
- },
- effects: [
- {
- key: '[PRIMARY_ELEMENTS_KEY]',
- namedEffect: {
- type: '[PRIMARY_EFFECT_TYPE]',
- [PRIMARY_EFFECT_PROPERTIES]
- },
- centeredToTarget: [PRIMARY_CENTERED]
- },
- {
- key: '[SECONDARY_ELEMENTS_KEY]',
- namedEffect: {
- type: '[SECONDARY_EFFECT_TYPE]',
- [SECONDARY_EFFECT_PROPERTIES]
- },
- centeredToTarget: [SECONDARY_CENTERED]
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[PRIMARY_ELEMENTS_KEY]`: Unique identifier for primary responsive elements
-- `[SECONDARY_ELEMENTS_KEY]`: Unique identifier for secondary responsive elements
-- `[PRIMARY_EFFECT_TYPE]`: Named effect for primary elements
-- `[SECONDARY_EFFECT_TYPE]`: Named effect for secondary elements
-- `[*_EFFECT_PROPERTIES]`: Properties specific to each effect type
-- `[*_CENTERED]`: Centering configuration for each element group
-- Other variables same as previous rules
-
-**Example - Interactive Card Grid**:
-
-```typescript
-{
- key: 'card-grid',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'grid-card',
- namedEffect: {
- type: 'Tilt3DMouse',
- angle: 12,
- perspective: 1000
- },
- centeredToTarget: true
- },
- {
- key: 'card-shadow',
- namedEffect: {
- type: 'AiryMouse',
- distance: { value: 20, unit: 'px' },
- axis: 'both'
- },
- centeredToTarget: true
- }
- ]
-}
-```
-
-**Example - Navigation Menu Response**:
-
-```typescript
-{
- key: 'nav-container',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'nav-item',
- namedEffect: {
- type: 'ScaleMouse',
- scale: 1.05,
- distance: { value: 80, unit: 'px' }
- },
- centeredToTarget: true
- },
- {
- key: 'nav-indicator',
- namedEffect: {
- type: 'TrackMouse',
- distance: { value: 15, unit: 'px' },
- axis: 'horizontal'
- },
- centeredToTarget: false
- }
- ]
-}
-```
-
----
-
-## Rule 7: Global Cursor Follower Effects with Named Effects
-
-**Use Case**: Page-wide cursor following elements that respond to mouse movement anywhere (e.g., custom cursors, global decorative followers, interactive overlays)
-
-**When to Apply**:
-
-- For custom cursor implementations
-- When creating global interactive overlays
-- For page-wide decorative followers
-- When building immersive cursor experiences
-
-**Pattern**:
-
-```typescript
-{
- key: '[FOLLOWER_KEY]',
- trigger: 'pointerMove',
- params: {
- hitArea: 'root'
- },
- effects: [
- {
- namedEffect: {
- type: '[FOLLOWER_EFFECT_TYPE]',
- distance: { value: [FOLLOWER_DISTANCE], unit: '[DISTANCE_UNIT]' },
- [FOLLOWER_PROPERTIES]
- },
- centeredToTarget: false,
- effectId: '[FOLLOWER_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[FOLLOWER_KEY]`: Unique identifier for cursor follower element
-- `[FOLLOWER_EFFECT_TYPE]`: 'TrackMouse', 'AiryMouse', 'BounceMouse'
-- `[FOLLOWER_DISTANCE]`: Distance/lag for follower (0 for perfect following)
-- `[FOLLOWER_PROPERTIES]`: Additional effect properties
-- `[FOLLOWER_EFFECT_ID]`: Unique identifier for the follower effect
-- Other variables same as previous rules
-
-**Example - Custom Cursor Follower**:
-
-```typescript
-{
- key: 'custom-cursor',
- trigger: 'pointerMove',
- params: {
- hitArea: 'root'
- },
- effects: [
- {
- namedEffect: {
- type: 'TrackMouse',
- distance: { value: 0, unit: 'px' },
- axis: 'both'
- },
- centeredToTarget: false,
- effectId: 'global-cursor'
- }
- ]
-}
-```
-
-**Example - Floating Decoration Follower**:
-
-```typescript
-{
- key: 'floating-decoration',
- trigger: 'pointerMove',
- params: {
- hitArea: 'root'
- },
- effects: [
- {
- namedEffect: {
- type: 'AiryMouse',
- distance: { value: 50, unit: 'px' },
- axis: 'both'
- },
- centeredToTarget: false,
- effectId: 'decoration-follower'
- }
- ]
-}
-```
-
----
-
-## Rule 8: Custom Pointer Effects with customEffect
-
-**Use Case**: When you need full control over pointer-driven animations that cannot be achieved with named effects, such as custom physics, complex multi-property animations, or unique visual transformations.
-
-**When to Apply**:
-
-- For custom physics-based animations
-- When creating unique visual effects not covered by named effects
-- When controlling WebGL/WebGPU effects or other JavaScript controlled effects
-- For complex DOM manipulations based on mouse position
-- When implementing grid-based or particle effects
-- For animations requiring access to velocity data
-
-**IMPORTANT**: Only use `customEffect` when `namedEffect` cannot achieve the desired result. Named effects are optimized and GPU-friendly.
-
-**Pattern - Basic customEffect**:
-
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'pointerMove',
- params: {
- hitArea: '[HIT_AREA]'
- },
- effects: [
- {
- key: '[TARGET_KEY]',
- customEffect: (element, progress) => {
- // progress.x: 0-1 horizontal position
- // progress.y: 0-1 vertical position
- // progress.v: { x, y } velocity (optional)
- // progress.active: boolean (optional)
-
- [CUSTOM_ANIMATION_LOGIC]
- },
- centeredToTarget: [CENTERED_TO_TARGET]
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[SOURCE_KEY]`: Unique identifier for source element tracking mouse movement
-- `[TARGET_KEY]`: Unique identifier for target element to animate
-- `[HIT_AREA]`: 'self' or 'root'
-- `[CUSTOM_ANIMATION_LOGIC]`: Your custom animation code using the progress object
-- `[CENTERED_TO_TARGET]`: true or false
-
-**Example - Custom Rotation Based on Mouse Position**:
-
-```typescript
-{
- key: 'rotation-container',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- customEffect: (element, progress) => {
- // Convert progress to angle (0-360 degrees)
- const angle = Math.atan2(
- progress.y - 0.5,
- progress.x - 0.5
- ) * (180 / Math.PI);
+---
- element.style.transform = `rotate(${angle}deg)`;
- },
- centeredToTarget: true
- }
- ]
-}
-```
+## PointerMoveParams
-**Example - Magnetic Effect with Distance Calculation**:
+`params` object for `pointerMove` interactions:
```typescript
-{
- key: 'magnetic-button',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- customEffect: (element, progress) => {
- // Calculate distance from center (0.5, 0.5)
- const dx = (progress.x - 0.5) * 2; // -1 to 1
- const dy = (progress.y - 0.5) * 2; // -1 to 1
-
- // Apply magnetic pull effect
- const maxMove = 20; // pixels
- const moveX = dx * maxMove;
- const moveY = dy * maxMove;
-
- element.style.transform = `translate(${moveX}px, ${moveY}px)`;
- },
- centeredToTarget: true
- }
- ]
-}
+type PointerMoveParams = {
+ hitArea?: 'root' | 'self';
+ axis?: 'x' | 'y';
+};
```
-**Example - Velocity-Based Motion Blur**:
+### Properties
-```typescript
-{
- key: 'velocity-element',
- trigger: 'pointerMove',
- params: {
- hitArea: 'root'
- },
- effects: [
- {
- customEffect: (element, progress) => {
- // Use velocity for motion blur intensity
- const velocity = progress.v || { x: 0, y: 0 };
- const speed = Math.sqrt(velocity.x ** 2 + velocity.y ** 2);
+- `hitArea` — determines where mouse movement is tracked:
+ - `'self'` — tracks mouse within the source element's bounds only. Use for local mouse-tracking effects on a specific element.
+ - `'root'` — tracks mouse anywhere in the viewport. Use for global cursor followers, ambient effects.
+- `axis` — restricts pointer tracking to a single axis. Only relevant when using `keyframeEffect`:
+ - `'x'` — maps horizontal pointer position to 0–1 progress for keyframe interpolation.
+ - `'y'` — maps vertical pointer position to 0–1 progress for keyframe interpolation. **Default** when `keyframeEffect` is used.
+ - When omitted with `namedEffect` or `customEffect`, both axes are available via the 2D progress object.
- // Apply blur based on speed
- const blurAmount = Math.min(speed * 0.5, 10);
- element.style.filter = `blur(${blurAmount}px)`;
+---
- // Move element towards mouse
- const offsetX = (progress.x - 0.5) * 100;
- const offsetY = (progress.y - 0.5) * 100;
- element.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
- },
- centeredToTarget: false
- }
- ]
-}
-```
+## Progress Object Structure
-**Example - Grid Cell Rotation Effect**:
+When using `customEffect` with `pointerMove`, the progress parameter is an object:
```typescript
-// First, cache grid cell positions for performance
-const cellCache = new Map();
-// Cache viewport size
-const windowWidth = window.innerWidth;
-const windowHeight = window.innerHeight;
-// ... populate cache with cell center positions
-// ... update `windowWidth/height` on window `resize` event
-
-{
- key: 'interactive-grid',
- trigger: 'pointerMove',
- params: {
- hitArea: 'root'
- },
- effects: [
- {
- customEffect: (element, progress) => {
- // Convert progress to viewport coordinates
- const mouseX = progress.x * windowWidth;
- const mouseY = progress.y * windowHeight;
+type Progress = {
+ x: number; // 0-1: horizontal position (0 = left edge, 1 = right edge)
+ y: number; // 0-1: vertical position (0 = top edge, 1 = bottom edge)
+ v?: {
+ // Velocity (optional)
+ x: number; // Horizontal velocity
+ y: number; // Vertical velocity
+ };
+ active?: boolean; // Whether mouse is currently in the hit area
+};
+```
- // Iterate through cached grid cells
- for (const [cell, cache] of cellCache) {
- const deltaX = mouseX - cache.x;
- const deltaY = mouseY - cache.y;
+---
- // Calculate angle pointing towards mouse
- const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI) + 90;
+## Centering with `centeredToTarget`
- // Calculate distance-based intensity
- const dist = Math.sqrt(deltaX ** 2 + deltaY ** 2);
- const intensity = Math.max(0, 1 - dist / 500);
+Controls how the progress range is calculated relative to the target element.
- cell.style.transform = `rotate(${angle}deg) scale(${1 + intensity * 0.2})`;
- }
- },
- centeredToTarget: false
- }
- ]
-}
-```
+Set `centeredToTarget: true` when:
-**Example - Active State Handling**:
+- The source and target are **different elements** (e.g., a container sources mouse tracking while a child element animates)
+- Using `hitArea: 'root'` with a specific target element — centers the coordinate origin on the target
+- Multiple effects target different elements from one source — each target gets its own centered coordinate space
+- The target element is offset from the source and needs progress values relative to its own center
-```typescript
-{
- key: 'active-aware-element',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- customEffect: (element, progress) => {
- if (!progress.active) {
- // Mouse left the hit area - reset or animate out
- element.style.transform = 'scale(1)';
- element.style.opacity = '0.7';
- return;
- }
+When `false` (or omitted), the source element's bounds are used for progress calculations. Use for cursor followers and global effects where progress should be relative to the hit area, not the target.
- // Mouse is active in hit area
- const scale = 1 + (1 - Math.abs(progress.x - 0.5) * 2) * 0.1;
- element.style.transform = `scale(${scale})`;
- element.style.opacity = '1';
- },
- centeredToTarget: true
- }
- ]
-}
-```
+---
-### customEffect with Transition Smoothing
+## Device Conditions
-For smoother animations, you can use `transitionDuration` and `transitionEasing`:
+`pointerMove` works best on hover-capable devices. Use a `conditions` entry with a `(hover: hover)` media query to prevent the interaction from registering on touch-only devices. On touch-only devices, consider a fallback to `viewEnter` or `viewProgress` based interactions:
```typescript
{
- key: 'smooth-custom',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
+ conditions: {
+ '[CONDITION_NAME]': { type: 'media', predicate: '(hover: hover)' }
},
- effects: [
+ interactions: [
{
- customEffect: (element, progress) => {
- const x = (progress.x - 0.5) * 50;
- const y = (progress.y - 0.5) * 50;
- element.style.transform = `translate(${x}px, ${y}px)`;
- },
- transitionDuration: 100,
- transitionEasing: 'easeOut',
- centeredToTarget: true
+ key: '[SOURCE_KEY]',
+ trigger: 'pointerMove',
+ conditions: ['[CONDITION_NAME]'],
+ params: { hitArea: '[HIT_AREA]' },
+ effects: [ /* ... */ ]
}
]
}
```
----
-
-## Rule 9: Multi-Element Custom Parallax with customEffect
+For devices with dynamic viewport sizes (e.g. mobile browsers where the address bar collapses), consider using viewport-relative units carefully and prefer `lvh`/`svh` over `dvh` unless dynamic viewport behavior is specifically desired.
-**Use Case**: Complex parallax effects with custom physics or non-standard transformations across multiple layers.
+---
-**When to Apply**:
+## Rule 1: namedEffect
-- For parallax with custom easing or physics
-- When layers need different calculation methods
-- For effects combining multiple CSS properties
+Use pre-built mouse presets from `@wix/motion-presets` that handle 2D mouse tracking internally. Mouse presets are preferred over `keyframeEffect` for 2D effects.
-**Pattern**:
+**Multiple effects:** The `effects` array can contain multiple effects — all share the same pointer tracking and fire together. Use this to animate different targets from the same pointer movement.
```typescript
{
- key: '[CONTAINER_KEY]',
+ key: '[SOURCE_KEY]',
trigger: 'pointerMove',
params: {
hitArea: '[HIT_AREA]'
},
effects: [
{
- key: '[LAYER_1_KEY]',
- customEffect: (element, progress) => {
- [LAYER_1_CUSTOM_LOGIC]
+ key: '[TARGET_KEY]',
+ namedEffect: {
+ type: '[NAMED_EFFECT_TYPE]',
+ [EFFECT_PROPERTIES]
},
- centeredToTarget: true
+ centeredToTarget: [CENTERED_TO_TARGET],
+ transitionDuration: [TRANSITION_DURATION_MS],
+ transitionEasing: '[TRANSITION_EASING]'
},
- {
- key: '[LAYER_2_KEY]',
- customEffect: (element, progress) => {
- [LAYER_2_CUSTOM_LOGIC]
- },
- centeredToTarget: true
- }
+ // additional effects targeting other elements can be added here
]
}
```
-**Example - Depth-Based Custom Parallax**:
+### Variables
-```typescript
-{
- key: 'parallax-scene',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'bg-stars',
- customEffect: (element, progress) => {
- // Background: subtle movement, inverted direction
- const x = (0.5 - progress.x) * 10;
- const y = (0.5 - progress.y) * 10;
- element.style.transform = `translate(${x}px, ${y}px)`;
- },
- centeredToTarget: true
- },
- {
- key: 'mid-clouds',
- customEffect: (element, progress) => {
- // Midground: moderate movement with rotation
- const x = (progress.x - 0.5) * 30;
- const y = (progress.y - 0.5) * 20;
- const rotation = (progress.x - 0.5) * 5;
- element.style.transform = `translate(${x}px, ${y}px) rotate(${rotation}deg)`;
- },
- centeredToTarget: true
- },
- {
- key: 'fg-elements',
- customEffect: (element, progress) => {
- // Foreground: strong movement with scale
- const x = (progress.x - 0.5) * 60;
- const y = (progress.y - 0.5) * 40;
- const scale = 1 + Math.abs(progress.x - 0.5) * 0.1;
- element.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
- },
- centeredToTarget: true
- }
- ]
-}
-```
+- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). The element that tracks mouse movement.
+- `[TARGET_KEY]` — identifier matching the element's key on the element to animate. Can be the same as source, or different when separating hit area from animation target.
+- `[HIT_AREA]` — `'self'` (mouse within source element) or `'root'` (mouse anywhere in viewport).
+- `[NAMED_EFFECT_TYPE]` — preset name from `@wix/motion-presets` mouse category:
+ - `'TrackMouse'` — follows the cursor with direct translation.
+ - `'Tilt3DMouse'` — tilts in 3D based on cursor position.
+ - `'Track3DMouse'` — translates and tilts in 3D following the cursor.
+ - `'SwivelMouse'` — tilts in 3D around a chosen pivot axis.
+ - `'AiryMouse'` — floats and rotates gently following the cursor.
+ - `'ScaleMouse'` — translates and scales uniformly following the cursor.
+ - `'BlurMouse'` — translates, tilts, scales, and blurs based on cursor distance.
+ - `'SkewMouse'` — translates and skews following the cursor.
+ - `'BlobMouse'` — translates and scales non-uniformly, creating a liquid-like deformation.
+ - Refer to motion-presets rules for detailed options of each preset. Do NOT guess preset option names/types; omit unknown options and rely on defaults.
+- `[CENTERED_TO_TARGET]` — `true` or `false`. See **Centering with `centeredToTarget`** above.
+- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates. Adds inertia to the effect.
+- `[TRANSITION_EASING]` — optional. Easing for the smoothing transition (e.g., `'easeOut'`). Adds a natural deceleration feel.
---
-## Rule 10: KeyframeEffect with Axis Mapping
-
-**Use Case**: When you want to use standard keyframe animations driven by pointer movement along a single axis (e.g., horizontal sliders, vertical progress indicators, single-axis parallax effects)
-
-**When to Apply**:
+## Rule 2: keyframeEffect with Single Axis
-- For slider-like interactions driven by horizontal mouse position
-- For vertical scroll-like effects driven by vertical mouse position
-- When you have existing keyframe animations you want to control with pointer movement
-- For simple linear interpolation effects along one axis
-
-**Pattern**:
+Use `keyframeEffect` when the pointer position along a single axis should drive a keyframe animation. The pointer's position on the chosen axis is mapped to linear 0–1 progress.
```typescript
{
@@ -1076,457 +166,147 @@ For smoother animations, you can use `transitionDuration` and `transitionEasing`
trigger: 'pointerMove',
params: {
hitArea: '[HIT_AREA]',
- axis: '[AXIS]' // 'x' or 'y'
+ axis: '[AXIS]'
},
effects: [
{
key: '[TARGET_KEY]',
keyframeEffect: {
- name: '[ANIMATION_NAME]',
+ name: '[EFFECT_NAME]',
keyframes: [
- { [PROPERTY]: '[START_VALUE]' },
- { [PROPERTY]: '[END_VALUE]' }
+ [START_KEYFRAME],
+ [CENTER_KEYFRAME],
+ [END_KEYFRAME]
]
},
- fill: '[FILL_MODE]',
- centeredToTarget: [CENTERED_TO_TARGET]
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[SOURCE_KEY]`: Unique identifier for source element tracking mouse movement
-- `[TARGET_KEY]`: Unique identifier for target element to animate
-- `[HIT_AREA]`: 'self' or 'root'
-- `[AXIS]`: 'x' (maps x position) or 'y' (maps y position) - **defaults to 'y'** (in `params`)
-- `[ANIMATION_NAME]`: Name for the keyframe animation
-- `[PROPERTY]`: CSS property to animate (transform, opacity, etc.)
-- `[START_VALUE]`: Value at progress 0 (left/top edge)
-- `[END_VALUE]`: Value at progress 1 (right/bottom edge)
-- `[FILL_MODE]`: 'none', 'forwards', 'backwards', 'both'
-- `[CENTERED_TO_TARGET]`: true or false
-
-**Example - Horizontal Slider with Multiple Targets**:
-
-This example shows a pointer-driven slider where the X position controls both a sliding element and an indicator's opacity/scale.
-
-```typescript
-{
- interactions: [
- {
- key: 'pointer-container',
- trigger: 'pointerMove',
- params: { hitArea: 'self', axis: 'x' },
- effects: [
- {
- key: 'pointer-slider',
- effectId: 'slide-effect',
- },
- {
- key: 'pointer-indicator',
- effectId: 'indicator-effect',
- },
- ],
- },
- ],
- effects: {
- 'slide-effect': {
- keyframeEffect: {
- name: 'slide-x',
- keyframes: [
- { transform: 'translateX(0px)' },
- { transform: 'translateX(220px)' },
- ],
- },
- fill: 'both',
- },
- 'indicator-effect': {
- keyframeEffect: {
- name: 'indicator-fade-scale',
- keyframes: [
- { opacity: '0.3', transform: 'scale(0.8)' },
- { opacity: '1', transform: 'scale(1)' },
- ],
- },
fill: 'both',
+ centeredToTarget: [CENTERED_TO_TARGET],
+ transitionDuration: [TRANSITION_DURATION_MS],
+ transitionEasing: '[TRANSITION_EASING]',
+ effectId: '[UNIQUE_EFFECT_ID]'
},
- },
+ // additional effects targeting other elements can be added here
+ ]
}
```
-**Important Notes**:
+### Variables
-- `axis` defaults to `'y'` when using `keyframeEffect` with `pointerMove`
-- For 2D effects that need both axes, you can use composite animations (Rule 11), `namedEffect`, or `customEffect`
+- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1.
+- `[HIT_AREA]` — `'self'` or `'root'`.
+- `[AXIS]` — `'x'` (horizontal) or `'y'` (vertical). Defaults to `'y'` when omitted.
+- `[EFFECT_NAME]` — unique string name for the keyframe effect.
+- `[START_KEYFRAME]` — CSS keyframe at progress 0 (left/top edge).
+- `[CENTER_KEYFRAME]` — optional. CSS keyframe at progress 0.5 (center).
+- `[END_KEYFRAME]` — CSS keyframe at progress 1 (right/bottom edge).
+- `[FILL_MODE]` — typically `'both'` to ensure the effect applies before entering and after exiting the effect's active range.
+- `[CENTERED_TO_TARGET]` — `true` or `false`.
+- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates.
+- `[TRANSITION_EASING]` — optional. Easing for the smoothing transition (e.g., `'easeOut'`).
+- `[UNIQUE_EFFECT_ID]` — optional string identifier.
---
-## Rule 11: Multi-Axis KeyframeEffect (X + Y)
-
-**Use Case**: Independent X/Y axis control using two `keyframeEffect` animations on the same target.
+## Rule 3: Two keyframeEffects with Two Axes and `composite`
-**Pattern**:
-Define two interactions on the same source/target pair—one for `axis: 'x'`, one for `axis: 'y'`. When animating the same CSS property (e.g. `transform`), use the `composite` option to combine the effects.
-
-**Example - 2D Scale Control**:
-
-X axis controls `scaleX`, Y axis controls `scaleY`.
+Use two separate interactions on the same source/target pair — one for `axis: 'x'`, one for `axis: 'y'` — for independent 2D control with keyframes. When both effects animate the same CSS property (e.g. `transform` or `filter`), use `composite` to combine them.
```typescript
{
interactions: [
{
- key: 'composite-add-container',
+ key: '[SOURCE_KEY]',
trigger: 'pointerMove',
- params: { hitArea: 'self', axis: 'x' },
- effects: [
- {
- key: 'composite-add-ball',
- effectId: 'scale-x-effect',
- },
- ],
+ params: { hitArea: '[HIT_AREA]', axis: 'x' },
+ effects: [{ key: '[TARGET_KEY]', effectId: '[X_EFFECT_ID]' }]
},
{
- key: 'composite-add-container',
+ key: '[SOURCE_KEY]',
trigger: 'pointerMove',
- params: { hitArea: 'self', axis: 'y' },
- effects: [
- {
- key: 'composite-add-ball',
- effectId: 'scale-y-effect',
- },
- ],
- },
+ params: { hitArea: '[HIT_AREA]', axis: 'y' },
+ effects: [{ key: '[TARGET_KEY]', effectId: '[Y_EFFECT_ID]' }]
+ }
],
effects: {
- 'scale-x-effect': {
+ '[X_EFFECT_ID]': {
keyframeEffect: {
- name: 'scale-x',
+ name: '[X_EFFECT_NAME]',
keyframes: [
- { transform: 'scaleX(0.5)' },
- { transform: 'scaleX(1.5)' },
- ],
+ { [PROPERTY]: '[X_START_VALUE]' },
+ { [PROPERTY]: '[X_CENTER_VALUE]' },
+ { [PROPERTY]: '[X_END_VALUE]' }
+ ]
},
- fill: 'both',
- composite: 'add',
+ fill: '[FILL_MODE]',
+ composite: '[COMPOSITE_OPERATION]',
+ transitionDuration: [TRANSITION_DURATION_MS],
+ transitionEasing: '[TRANSITION_EASING]'
},
- 'scale-y-effect': {
+ '[Y_EFFECT_ID]': {
keyframeEffect: {
- name: 'scale-y',
+ name: '[Y_EFFECT_NAME]',
keyframes: [
- { transform: 'scaleY(0.5)' },
- { transform: 'scaleY(1.5)' },
- ],
+ { [PROPERTY]: '[Y_START_VALUE]' },
+ { [PROPERTY]: '[Y_CENTER_VALUE]' },
+ { [PROPERTY]: '[Y_END_VALUE]' }
+ ]
},
- fill: 'both',
- composite: 'add',
- },
- },
-}
-```
-
----
-
-## Advanced Patterns and Combinations
-
-### Responsive Pointer Effects
-
-`pointerMove` only fires on pointer-capable devices, but touch users still visit the page. Use the `conditions` config map to define device/motion guards — condition IDs are arbitrary strings you define, matched against the media query predicates you provide.
-
-```typescript
-{
- conditions: {
- // Only run pointer effects on devices that support hover (non-touch)
- 'supports-hover': { type: 'media', predicate: '(hover: hover)' },
- // Suppress animations for users who prefer reduced motion
- 'prefers-motion': { type: 'media', predicate: '(prefers-reduced-motion: no-preference)' },
- },
- interactions: [
- {
- key: 'responsive-element',
- trigger: 'pointerMove',
- conditions: ['supports-hover', 'prefers-motion'],
- params: { hitArea: 'self' },
- effects: [
- {
- key: 'responsive-element',
- namedEffect: { type: 'Tilt3DMouse', angle: 20, perspective: 800 },
- centeredToTarget: true
- }
- ]
+ fill: '[FILL_MODE]',
+ composite: '[COMPOSITE_OPERATION]',
+ transitionDuration: [TRANSITION_DURATION_MS],
+ transitionEasing: '[TRANSITION_EASING]'
}
- ]
+ }
}
```
-### Contextual Hit Areas
+### Variables
-Different hit areas for different interaction contexts:
+- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1.
+- `[HIT_AREA]` — `'self'` or `'root'`.
+- `[X_EFFECT_ID]` / `[Y_EFFECT_ID]` — unique string identifiers for the X-axis and Y-axis effects, referenced from the top-level `effects` map.
+- `[X_EFFECT_NAME]` / `[Y_EFFECT_NAME]` — unique string names for each keyframe effect.
+- `[PROPERTY]` — CSS property animated by both effects (e.g., `transform`).
+- `[X_START_VALUE]` / `[X_CENTER_VALUE]` / `[X_END_VALUE]` — CSS values for the X-axis range. The `CENTER` keyframe is optional.
+- `[Y_START_VALUE]` / `[Y_CENTER_VALUE]` / `[Y_END_VALUE]` — CSS values for the Y-axis range. The `CENTER` keyframe is optional.
+- `[COMPOSITE_OPERATION]` — `'add'` or `'accumulate'`. Required when both effects animate the same property so their values combine rather than override each other. `'add'`: function values add up sequentially. `'accumulate'`: similar functions' arguments add up.
+- `[FILL_MODE]` — typically `'both'` to ensure the effect applies before entering and after exiting the effect's active range.
+- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates.
+- `[TRANSITION_EASING]` — optional. Easing for the smoothing transition (e.g., `'easeOut'`).
-```typescript
-// Local interaction - mouse must be over element
-{
- key: 'local-card',
- trigger: 'pointerMove',
- params: {
- hitArea: 'self'
- },
- effects: [
- {
- key: 'local-card',
- namedEffect: {
- type: 'Tilt3DMouse',
- angle: 15
- },
- centeredToTarget: true
- }
- ]
-},
-// Global interaction - responds to mouse anywhere
-{
- key: 'global-background',
- trigger: 'pointerMove',
- params: {
- hitArea: 'root'
- },
- effects: [
- {
- key: 'ambient-element',
- namedEffect: {
- type: 'AiryMouse',
- distance: { value: 30, unit: 'px' }
- },
- centeredToTarget: false
- }
- ]
-}
-```
+---
-### Axis-Constrained Effects
+## Rule 4: customEffect
-Controlling movement direction for specific design needs:
+Use `customEffect` when you need full imperative control over pointer-driven animations — custom physics, complex multi-property animations, velocity-reactive effects, or controlling WebGL/WebGPU and other JavaScript-driven effects. The callback receives the 2D progress object (see **Progress Object Structure**).
```typescript
{
- key: 'constrained-container',
+ key: '[SOURCE_KEY]',
trigger: 'pointerMove',
params: {
- hitArea: 'self'
+ hitArea: '[HIT_AREA]'
},
effects: [
{
- key: 'horizontal-slider',
- namedEffect: {
- type: 'TrackMouse',
- distance: { value: 100, unit: 'px' },
- axis: 'horizontal'
+ key: '[TARGET_KEY]',
+ customEffect: (element: Element, progress: Progress) => {
+ [CUSTOM_ANIMATION_LOGIC]
},
- centeredToTarget: true
+ centeredToTarget: [CENTERED_TO_TARGET],
+ transitionDuration: [TRANSITION_DURATION_MS],
+ transitionEasing: '[TRANSITION_EASING]'
},
- {
- key: 'vertical-indicator',
- namedEffect: {
- type: 'ScaleMouse',
- scale: 1.2,
- distance: { value: 150, unit: 'px' },
- axis: 'vertical'
- },
- centeredToTarget: true
- }
+ // additional effects targeting other elements can be added here
]
}
```
----
-
-## Best Practices for PointerMove Interactions
-
-### Effect Type Selection Guidelines
-
-**When to use `namedEffect` (Preferred)**:
-
-1. For standard mouse-tracking effects (tilt, track, scale, blur)
-2. When GPU-optimized performance is critical
-3. For effects that match preset behavior (3D tilt, elastic following)
-4. When you don't need custom physics or calculations
-
-**When to use `customEffect`**:
-
-1. For custom physics-based animations (springs, gravity)
-2. When you need access to velocity data (`progress.v`)
-3. For complex DOM manipulations (updating multiple elements)
-4. When creating effects not covered by named presets
-5. For grid/particle systems with many elements
-6. For controlling WebGL/WebGPU effects
-
-**When to use `keyframeEffect`**:
-
-1. When you want single-axis control using the `axis` parameter ('x' or 'y')
-2. For slider-like interactions driven by pointer position along one axis
-3. For 2D control, use two `keyframeEffect` interactions with `composite` (see Rule 11)
-
-### Performance Guidelines
-
-1. **Limit simultaneous pointer effects** - too many can cause performance issues
-2. **Test on various devices** - pointer sensitivity varies across hardware
-3. **Cache DOM queries outside `customEffect` callbacks** - avoid repeated `querySelector` calls inside the callback
-4. **Use `requestAnimationFrame` sparingly** - the library already handles frame timing
-5. **Prefer `namedEffect` over `customEffect`** - named effects are optimized for GPU acceleration
-
-### Hit Area Guidelines
-
-1. **Use `hitArea: 'self'`** for local element interactions (cards, buttons, hover effects)
-2. **Use `hitArea: 'root'`** for global cursor followers and ambient effects
-3. **Consider container boundaries** when choosing hit areas
-4. **Test hit area responsiveness** across different screen sizes
-5. **`'self'`** is more performant than `'root'` - use when possible
-
-### Centering Guidelines
-
-1. **Set `centeredToTarget: true`** when target differs from source (e.g., animating child element from parent)
-2. **Use `centeredToTarget: false`** for cursor followers and global effects
-3. **Test centering behavior** with different element sizes
-4. **Consider responsive design** when setting centering
-5. **Centering affects how progress.x/y map to element position**
-
-### Common Use Cases by Pattern
-
-**Single Element 3D Effects (Rule 1)** - `namedEffect`:
-
-- Interactive product cards
-- 3D showcase elements
-- Immersive button interactions
-- Portfolio item presentations
-
-**Movement Followers (Rule 2)** - `namedEffect`:
-
-- Cursor follower elements
-- Floating decorative elements
-- Responsive UI indicators
-- Interactive overlays
-
-**Scale & Deformation (Rule 3)** - `namedEffect`:
-
-- Organic interface elements
-- Interactive morphing shapes
-- Creative scaling buttons
-- Blob-like interactions
-
-**Visual Effects (Rule 4)** - `namedEffect`:
-
-- Creative interface elements
-- Motion blur interactions
-- Spinning decorative elements
-- Dynamic visual feedback
-
-**Multi-Element Parallax (Rule 5)** - `namedEffect`:
-
-- Layered background effects
-- Depth-based interactions
-- Immersive hero sections
-- Complex scene responses
-
-**Group Coordination (Rule 6)** - `namedEffect`:
-
-- Interactive card grids
-- Navigation menu systems
-- Gallery hover effects
-- Coordinated UI responses
-
-**Global Followers (Rule 7)** - `namedEffect`:
-
-- Custom cursor implementations
-- Page-wide decorative elements
-- Global interactive overlays
-- Immersive cursor experiences
-
-**Custom Pointer Effects (Rule 8)** - `customEffect`:
-
-- Grid-based rotation systems
-- Magnetic pull/push effects
-- Physics-based animations
-- Velocity-reactive effects
-- Complex DOM manipulations
-- Particle systems
-
-**Multi-Element Custom Parallax (Rule 9)** - `customEffect`:
-
-- Non-linear parallax physics
-- Layers with different calculation methods
-- Combined transform effects per layer
-- Custom easing per element
-
-**Single-Axis Keyframe Control (Rule 10)** - `keyframeEffect`:
-
-- Horizontal slider interactions
-- Vertical progress indicators
-- Single-axis reveal effects
-- Linear interpolation along one axis
-
-**Composite Keyframe (Rule 11)** - Two `keyframeEffect` + `composite`:
-
-- 2D element positioning with pointer
-- Combined X/Y transform animations
-- Independent axis control with keyframes
-- Declarative 2D animations without customEffect
-
-### Troubleshooting Common Issues
-
-**Poor pointer responsiveness**:
-
-- Verify `hitArea` configuration
-- Test `centeredToTarget` settings
-- Ensure target elements are properly positioned
-
-**Performance issues**:
-
-- Reduce number of simultaneous effects
-- Use simpler named effects
-- Check for CSS conflicts
-- Test on lower-end devices
-- In customEffect: cache DOM queries outside the callback
-- Avoid creating objects inside customEffect callbacks
-
-**customEffect not updating smoothly**:
-
-- Add `transitionDuration` and `transitionEasing` for smoother transitions
-- Avoid expensive calculations inside the callback
-- Consider debouncing complex logic
-
-**customEffect progress values unexpected**:
-
-- Remember x/y are 0-1 normalized (not pixel values)
-- Check `centeredToTarget` setting affects coordinate mapping
-- Verify `hitArea` matches expected tracking area
-- Use `progress.active` to handle edge cases
-
-**Unexpected behavior on touch devices**:
-
-- Implement appropriate conditions for touch vs. mouse
-- Provide touch-friendly alternatives
-- Test pointer events on mobile devices
-- Consider disabling complex effects on touch
-
-**Effects not triggering**:
-
-- Verify source element exists and is visible
-- Check `data-interact-key` matches CSS selector
-- Ensure proper hit area configuration
-- Test mouse event propagation
-
----
-
-## Quick Reference: Effect Type Selection
+### Variables
-| Requirement | Use This | Why |
-| --------------------------- | ------------------------------------------ | ------------------------------------------------------ |
-| Standard 3D tilt | `namedEffect: { type: 'Tilt3DMouse' }` | GPU-optimized, battle-tested |
-| Cursor following | `namedEffect: { type: 'TrackMouse' }` | Built-in physics |
-| Horizontal progress control | `keyframeEffect` + `params: { axis: 'x' }` | Maps x position to keyframes |
-| Vertical progress control | `keyframeEffect` + `params: { axis: 'y' }` | Maps y position to keyframes |
-| Multi-axis keyframe (X + Y) | Two interactions with `keyframeEffect` | Use `composite: 'add'` or `'accumulate'` for same prop |
-| Custom physics | `customEffect` | Full control over calculations |
-| Velocity-based effects | `customEffect` | Access to `progress.v` |
-| Grid/particle systems | `customEffect` | Can manipulate many elements |
+- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1.
+- `[HIT_AREA]` — `'self'` or `'root'`.
+- `[CUSTOM_ANIMATION_LOGIC]` — JavaScript using `progress.x`, `progress.y`, `progress.v`, and `progress.active` to apply the effect.
+- `[CENTERED_TO_TARGET]` — `true` or `false`.
+- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates.
+- `[TRANSITION_EASING]` — optional. Easing for the smoothing transition (e.g., `'easeOut'`).
diff --git a/packages/interact/rules/scroll-list.md b/packages/interact/rules/scroll-list.md
deleted file mode 100644
index 86c7955c..00000000
--- a/packages/interact/rules/scroll-list.md
+++ /dev/null
@@ -1,748 +0,0 @@
-# Scroll List Animation Rules for @wix/interact
-
-Scroll-driven list animations using `@wix/interact`. Sticky hierarchy: **container** → **items** → **content**. Use `key` for container/item; use `selector` for content within an item.
-
-## Rule 1: Sticky Container List Animations with Named Effects
-
-**Use Case**: Sticky list containers with named effects (horizontal galleries, parallax backgrounds). Use `contain` range—animations run while the element is stuck in position.
-
-**When to Apply**: Sticky container sliding, parallax, background transformations.
-
-**Pattern**:
-
-```typescript
-{
- key: '[CONTAINER_KEY]',
- trigger: 'viewProgress',
- effects: [
- {
- key: '[CONTAINER_KEY]',
- namedEffect: {
- type: '[CONTAINER_NAMED_EFFECT]'
- },
- rangeStart: { name: 'contain', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
- rangeEnd: { name: 'contain', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
- easing: 'linear',
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**: `[CONTAINER_KEY]`, `[CONTAINER_NAMED_EFFECT]` ('BgParallax', 'PanScroll', 'MoveScroll', 'ParallaxScroll', 'BgPan', 'BgZoom', 'BgFade', 'BgReveal'), `[START_PERCENTAGE]`/`[END_PERCENTAGE]` (typically 0/100), `[UNIQUE_EFFECT_ID]`.
-
-**Example - Horizontal Sliding Gallery Container**:
-
-```typescript
-{
- key: 'gallery-container',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'gallery-container',
- namedEffect: {
- type: 'PanScroll'
- },
- rangeStart: { name: 'contain', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'contain', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear',
- effectId: 'gallery-slide'
- }
- ]
-}
-```
-
-**Example - Parallax Container Background**:
-
-```typescript
-{
- key: 'sticky-list-wrapper',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'list-background',
- namedEffect: {
- type: 'BgParallax'
- },
- rangeStart: { name: 'contain', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'contain', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear',
- effectId: 'bg-parallax'
- }
- ]
-}
-```
-
----
-
-## Rule 2: Sticky Item List Animations with Named Effects
-
-**Use Case**: Individual sticky list items with named effects for entrance/exit (progressive reveals, item transformations).
-
-**When to Apply**: Item entrance/exit during sticky phases, progressive item reveals.
-
-**Pattern**:
-
-```typescript
-{
- key: '[ITEM_KEY]',
- trigger: 'viewProgress',
- effects: [
- {
- key: '[ITEM_KEY]',
- namedEffect: {
- type: '[ITEM_NAMED_EFFECT]'
- },
- rangeStart: { name: '[RANGE_TYPE]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
- rangeEnd: { name: '[RANGE_TYPE]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
- easing: '[EASING_FUNCTION]',
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[ITEM_KEY]`: Individual list item identifier
-- `[ITEM_NAMED_EFFECT]`: Item-level scroll effects from @wix/motion-presets:
- - **Reveal/Fade**: 'FadeScroll', 'BlurScroll', 'RevealScroll', 'ShapeScroll', 'ShuttersScroll'
- - **Movement**: 'MoveScroll', 'SlideScroll', 'PanScroll', 'SkewPanScroll'
- - **Scale**: 'GrowScroll', 'ShrinkScroll', 'StretchScroll'
- - **Rotation**: 'SpinScroll', 'FlipScroll', 'TiltScroll', 'TurnScroll'
- - **3D**: 'ArcScroll', 'Spin3dScroll'
-- `[START_PERCENTAGE]`: Range start percentage (0-100)
-- `[END_PERCENTAGE]`: Range end percentage (0-100)
-- `[EASING_FUNCTION]`: Timing function
-
-**Example - Item Entrance Reveal**:
-
-```typescript
-{
- key: 'list-item',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'list-item',
- namedEffect: {
- type: 'RevealScroll',
- direction: 'bottom'
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 60 } },
- easing: 'ease-out',
- effectId: 'item-reveal'
- }
- ]
-}
-```
-
-**Example - Item Scale During Sticky**:
-
-```typescript
-{
- key: 'sticky-list-item',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'sticky-list-item',
- namedEffect: {
- type: 'GrowScroll'
- },
- rangeStart: { name: 'contain', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'contain', offset: { unit: 'percentage', value: 50 } },
- easing: 'ease-in-out',
- effectId: 'item-grow'
- }
- ]
-}
-```
-
----
-
-## Rule 3: Sticky Item List Content Animations with Named Effects
-
-**Use Case**: Content within sticky items; each item is the viewProgress trigger (text reveals in cards, image animations, progressive disclosure). Use `key` for the item, `selector` for content within.
-
-**When to Apply**: Content within sticky items, staggered content reveals, text/image animations inside list items.
-
-**Pattern**:
-
-```typescript
-{
- key: '[ITEM_CONTAINER_KEY]',
- trigger: 'viewProgress',
- effects: [
- {
- key: '[CONTENT_KEY]',
- namedEffect: {
- type: '[CONTENT_NAMED_EFFECT]'
- },
- rangeStart: { name: '[RANGE_TYPE]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
- rangeEnd: { name: '[RANGE_TYPE]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
- easing: '[EASING_FUNCTION]',
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[ITEM_CONTAINER_KEY]` / `[CONTENT_KEY]`: Item and content identifiers. Use `selector` (e.g. `selector: '.content-text'`) for content within the item.
-- `[CONTENT_NAMED_EFFECT]`: Content-level scroll effects from @wix/motion-presets:
- - **Opacity/Visibility**: 'FadeScroll', 'BlurScroll'
- - **Reveal**: 'RevealScroll', 'ShapeScroll', 'ShuttersScroll'
- - **3D Transforms**: 'TiltScroll', 'FlipScroll', 'ArcScroll', 'TurnScroll', 'Spin3dScroll'
- - **Movement**: 'MoveScroll', 'SlideScroll'
- - **Scale**: 'GrowScroll', 'ShrinkScroll'
-
-**Example - Staggered Text Content Reveal**:
-
-```typescript
-{
- key: 'list-item-1',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'list-item-1',
- selector: '.content-text',
- namedEffect: {
- type: 'FadeScroll'
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 20 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 80 } },
- easing: 'ease-out',
- effectId: 'text-reveal-1'
- }
- ]
-},
-{
- key: 'list-item-2',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'list-item-2',
- selector: '.content-text',
- namedEffect: {
- type: 'FadeScroll'
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 20 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 80 } },
- easing: 'ease-out',
- effectId: 'text-reveal-2'
- }
- ]
-}
-```
-
-**Example - Image Animation Within List Item**:
-
-```typescript
-{
- key: 'product-card',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'product-card',
- selector: '.hero-image',
- namedEffect: {
- type: 'RevealScroll'
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 50 } },
- easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
- effectId: 'product-image-reveal'
- }
- ]
-}
-```
-
----
-
-## Rule 4: List Container Keyframe Animations
-
-**Use Case**: Custom container keyframe effects for sticky containers (multi-property transforms, complex backgrounds).
-
-**When to Apply**: Custom container effects not available in named effects.
-
-**Pattern**:
-
-```typescript
-{
- key: '[CONTAINER_KEY]',
- trigger: 'viewProgress',
- effects: [
- {
- key: '[CONTAINER_KEY]',
- keyframeEffect: {
- name: '[UNIQUE_KEYFRAME_EFFECT_NAME]',
- keyframes: [
- { [CSS_PROPERTY_1]: '[START_VALUE_1]', [CSS_PROPERTY_2]: '[START_VALUE_2]', [CSS_PROPERTY_3]: '[START_VALUE_3]' },
- { [CSS_PROPERTY_1]: '[MID_VALUE_1]' },
- { [CSS_PROPERTY_1]: '[END_VALUE_1]', [CSS_PROPERTY_2]: '[END_VALUE_2]', [CSS_PROPERTY_3]: '[END_VALUE_3]' }
- ]
- },
- rangeStart: { name: 'contain', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
- rangeEnd: { name: 'contain', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
- easing: 'linear',
- fill: 'both',
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**: `[CONTAINER_KEY]`, `[UNIQUE_KEYFRAME_EFFECT_NAME]` (or `[UNIQUE_EFFECT_ID]`). Other variables same as Rule 1.
-
-**Example - Multi-Property Container Animation**:
-
-```typescript
-{
- key: 'feature-list-container',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'feature-list-container',
- keyframeEffect: {
- name: 'container-slide',
- keyframes: [
- { transform: 'translateX(0)', filter: 'brightness(1)', backgroundColor: 'rgb(255 255 255 / 0)' },
- { transform: 'translateX(-50%)', filter: 'brightness(1.2)', backgroundColor: 'rgb(255 255 255 / 0.1)' },
- { transform: 'translateX(-100%)', filter: 'brightness(1)', backgroundColor: 'rgb(255 255 255 / 0)' }
- ]
- },
- rangeStart: { name: 'contain', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'contain', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear',
- fill: 'both',
- effectId: 'container-slide'
- }
- ]
-}
-```
-
-**Example - Container Background Transformation**:
-
-```typescript
-{
- key: 'gallery-wrapper',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'gallery-background',
- keyframeEffect: {
- name: 'bg-transform',
- keyframes: [
- { transform: 'scale(1.1) rotate(6deg)', opacity: '0.8', filter: 'hue-rotate(30deg)' },
- { transform: 'scale(1) rotate(0deg)', opacity: '1', filter: 'hue-rotate(0deg)' }
- ]
- },
- rangeStart: { name: 'contain', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'contain', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear',
- fill: 'both',
- effectId: 'bg-transform'
- }
- ]
-}
-```
-
----
-
-## Rule 5: List Item Keyframe Entrance/Exit Animations
-
-**Use Case**: Custom keyframe entrance/exit for list items (complex reveals, dismissals).
-
-**When to Apply**: Complex item entrance effects beyond named effects, coordinating item wrapper with content animations.
-
-**Pattern**:
-
-```typescript
-{
- key: '[ITEM_KEY]',
- trigger: 'viewProgress',
- effects: [
- {
- key: '[ITEM_KEY]',
- keyframeEffect: {
- name: '[UNIQUE_KEYFRAME_EFFECT_NAME]',
- keyframes: [
- { [CSS_PROPERTY_1]: '[START_VALUE_1]', [CSS_PROPERTY_2]: '[START_VALUE_2]' },
- { [CSS_PROPERTY_1]: '[MID_VALUE_1]' },
- { [CSS_PROPERTY_1]: '[END_VALUE_1]', [CSS_PROPERTY_2]: '[END_VALUE_2]' }
- ]
- },
- rangeStart: { name: '[RANGE_TYPE]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
- rangeEnd: { name: '[RANGE_TYPE]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
- easing: '[EASING_FUNCTION]',
- fill: 'both',
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**: `[ITEM_KEY]`, `[EASING_FUNCTION]`. Other variables same as Rule 4.
-
-**Example - Complex Item Entrance**:
-
-```typescript
-{
- key: 'timeline-item',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'timeline-item',
- keyframeEffect: {
- name: 'timeline-entrance',
- keyframes: [
- { opacity: '0', transform: 'translateY(100px) scale(0.8) rotate(5deg)', filter: 'blur(10px)', boxShadow: '0 0 0 rgb(0 0 0 / 0)' },
- { opacity: '0.5', transform: 'translateY(20px) scale(0.95) rotate(1deg)', filter: 'blur(2px)', boxShadow: '0 10px 20px rgb(0 0 0 / 0.1)' },
- { opacity: '1', transform: 'translateY(0) scale(1) rotate(0deg)', filter: 'blur(0)', boxShadow: '0 20px 40px rgb(0 0 0 / 0.15)' }
- ]
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 80 } },
- easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
- fill: 'both',
- effectId: 'timeline-entrance'
- }
- ]
-}
-```
-
-**Example - Item Exit Sequence**:
-
-```typescript
-{
- key: 'card-item',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'card-item',
- keyframeEffect: {
- name: 'card-exit-6',
- keyframes: [
- { opacity: '1', transform: 'scale(1) rotate(0deg)', filter: 'brightness(1)' },
- { opacity: '0.7', transform: 'scale(0.9) rotate(-2deg)', filter: 'brightness(0.8)' },
- { opacity: '0', transform: 'scale(0.8) rotate(-5deg)', filter: 'brightness(0.6)' }
- ]
- },
- rangeStart: { name: 'exit', offset: { unit: 'percentage', value: 20 } },
- rangeEnd: { name: 'exit', offset: { unit: 'percentage', value: 100 } },
- easing: 'ease-in',
- fill: 'both',
- effectId: 'card-exit'
- }
- ]
-}
-```
-
----
-
-## Rule 6: Staggered List Animations with Custom Timing
-
-**Use Case**: Coordinated animations across list items; each item is the viewProgress trigger. Shared `effectId` in effects registry.
-
-**When to Apply**: Wave-like propagation, linear/exponential stagger, reverse-order exit effects. Uses shared `effectId` in the effects registry so each item references the same effect.
-
-**Pattern**:
-
-```typescript
-{
- effects: {
- [EFFECT_ID]: {
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
- rangeStart: { name: '[RANGE_TYPE]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
- rangeEnd: { name: '[RANGE_TYPE]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
- easing: '[EASING_FUNCTION]'
- }
- },
- interactions: [
- {
- key: '[ITEM_KEY_N]',
- trigger: 'viewProgress',
- effects: [
- {
- effectId: '[EFFECT_ID]'
- }
- ]
- },
- // ... repeat for each item
- ]
-}
-```
-
-**Example - Linear Staggered Card Entrance**:
-
-```typescript
-{
- effects: {
- 'card-entrance': {
- namedEffect: {
- type: 'SlideScroll'
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 60 } },
- easing: 'linear'
- }
- },
- interactions: [
- {
- key: 'card-1',
- trigger: 'viewProgress',
- effects: [
- {
- effectId: 'card-entrance'
- }
- ]
- },
- {
- key: 'card-2',
- trigger: 'viewProgress',
- effects: [
- {
- effectId: 'card-entrance'
- }
- ]
- },
- {
- key: 'card-3',
- trigger: 'viewProgress',
- effects: [
- {
- effectId: 'card-entrance'
- }
- ]
- },
- ]
-}
-```
-
-**Example - Exponential Stagger for Dramatic Effect**:
-
-```typescript
-{
- effects: {
- 'feature-entrance': {
- keyframeEffect: {
- name: 'feature-entrance',
- keyframes: [
- { opacity: '0', transform: 'translateY(50px) scale(0.9)' },
- { opacity: '1', transform: 'translateY(0) scale(1)' }
- ]
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 100 } },
- easing: 'expoOut',
- fill: 'both'
- }
- },
- interactions: [
- { key: 'feature-1', trigger: 'viewProgress', effects: [{ effectId: 'feature-entrance' }] },
- { key: 'feature-2', trigger: 'viewProgress', effects: [{ effectId: 'feature-entrance' }] },
- { key: 'feature-3', trigger: 'viewProgress', effects: [{ effectId: 'feature-entrance' }] },
- ]
-}
-```
-
----
-
-## Rule 7: Dynamic Content Animations with Custom Effects
-
-**Use Case**: Per-item dynamic content via `customEffect` (counters, progress tracking, data visualization, dynamic text).
-
-**When to Apply**: Scroll-driven counters, progress tracking, data visualization, dynamic text updates in list contexts.
-
-**Pattern**:
-
-```typescript
-{
- key: '[LIST_CONTAINER_KEY]',
- trigger: 'viewProgress',
- effects: [
- {
- key: '[DYNAMIC_CONTENT_KEY]',
- customEffect: (element, progress) => {
- // progress is 0-1 representing scroll position within range
- [CUSTOM_CALCULATION_LOGIC]
- [DYNAMIC_CONTENT_UPDATE]
- [VISUAL_PROPERTY_UPDATES]
- },
- rangeStart: { name: '[RANGE_TYPE]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
- rangeEnd: { name: '[RANGE_TYPE]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
- fill: 'both',
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**: `[LIST_CONTAINER_KEY]` / `[DYNAMIC_CONTENT_KEY]` identify the list and target elements. The `customEffect` receives `(element, progress)` where progress is 0–1.
-
-**Example - Scroll-Driven Counter in List**:
-
-```typescript
-{
- key: 'stats-list-container',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'stat-counter',
- customEffect: (element, progress) => {
- const targetValue = parseInt(element.dataset.targetValue) || 100;
- const currentValue = Math.floor(targetValue * progress);
- const percentage = Math.floor(progress * 100);
-
- // Update counter text
- element.textContent = currentValue.toLocaleString();
-
- // Update visual properties based on progress
- element.style.color = `hsl(${progress * 120}, 70%, 50%)`; // Green to red progression
- element.style.transform = `scale(${0.8 + progress * 0.2})`; // Subtle scale effect
-
- // Update progress bar if exists
- const progressBar = element.querySelector('.progress-bar');
- if (progressBar) {
- progressBar.style.width = `${percentage}%`;
- }
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'exit', offset: { unit: 'percentage', value: 100 } },
- fill: 'both',
- effectId: 'stats-counter'
- }
- ]
-}
-```
-
-**Example - Interactive List Progress Tracking**:
-
-```typescript
-{
- key: 'task-list',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'task-item',
- customEffect: (element, progress) => {
- const items = element.closest('interact-element')?.querySelectorAll('.task-item') || [];
- const totalItems = items.length;
- const elementIndex = Array.from(items).indexOf(element);
- const itemStartProgress = elementIndex / totalItems;
- const itemEndProgress = (elementIndex + 1) / totalItems;
- let itemProgress = progress > itemStartProgress
- ? Math.min(1, (progress - itemStartProgress) / (itemEndProgress - itemStartProgress))
- : 0;
-
- const checkbox = element.querySelector('.task-checkbox');
- const taskText = element.querySelector('.task-text');
- if (itemProgress > 0.5) {
- element.classList.add('active');
- checkbox.style.transform = `scale(${0.8 + itemProgress * 0.4})`;
- checkbox.style.opacity = itemProgress;
- }
- if (itemProgress > 0.8) {
- element.classList.add('completed');
- taskText.style.textDecoration = 'line-through';
- taskText.style.opacity = '0.7';
- }
- },
- rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 100 } },
- fill: 'both',
- effectId: 'task-progress'
- }
- ]
-}
-```
-
----
-
-## Advanced Patterns and Combinations
-
-### Multi-Layer List Coordination
-
-Container, items, and content: use `cover` for background/foreground layers (full scroll range), `contain` for the sticky container layer (while stuck).
-
-```typescript
-{
- key: 'complex-list-section',
- trigger: 'viewProgress',
- effects: [
- { key: 'list-background', keyframeEffect: { name: 'bg-parallax', keyframes: [{ transform: 'scale(1.1)' }, { transform: 'scale(1) translateY(-50px)' }] }, rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 0 } }, rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 100 } }, easing: 'linear', fill: 'both' },
- { key: 'list-container', keyframeEffect: { name: 'container-slide', keyframes: [{ transform: 'translateX(0)' }, { transform: 'translateX(-50%)' }] }, rangeStart: { name: 'contain', offset: { unit: 'percentage', value: 0 } }, rangeEnd: { name: 'contain', offset: { unit: 'percentage', value: 100 } }, easing: 'linear', fill: 'both' }
- ]
-}
-```
-
-### Responsive List Animations
-
-Condition IDs are user-defined strings declared in the top-level `conditions` map. Define separate interactions for the same `key` with different conditions and effects.
-
-```typescript
-{
- conditions: {
- 'desktop-only': { type: 'media', predicate: '(min-width: 768px)' },
- 'prefers-motion': { type: 'media', predicate: '(prefers-reduced-motion: no-preference)' },
- 'mobile-only': { type: 'media', predicate: '(max-width: 767px)' },
- },
- interactions: [
- {
- key: 'list-item',
- trigger: 'viewProgress',
- conditions: ['desktop-only', 'prefers-motion'],
- effects: [
- {
- key: 'list-item',
- keyframeEffect: {
- name: 'item-complex',
- keyframes: [
- { opacity: '0', transform: 'translateY(-20px) rotateY(5deg)' },
- { opacity: '1', transform: 'translateY(0) rotateY(0deg)' }
- ]
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 80 } },
- easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
- fill: 'both'
- }
- ]
- },
- // Simplified fallback for mobile or reduced-motion users
- {
- key: 'list-item',
- trigger: 'viewProgress',
- conditions: ['mobile-only'],
- effects: [
- {
- key: 'list-item',
- keyframeEffect: {
- name: 'item-simple',
- keyframes: [
- { opacity: '0', transform: 'translateY(30px)' },
- { opacity: '1', transform: 'translateY(0)' }
- ]
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 60 } },
- easing: 'ease-out',
- fill: 'both'
- }
- ]
- }
- ]
-}
-```
-
----
-
-## Best Practices for List Scroll Animations
-
-### List-Specific Guidelines
-
-1. **Sticky hierarchy**: Container → items → content. Use `contain` range for sticky container effects (animations run while the element is stuck in position).
-2. **Content coordination**: Use same timeline with `cover`/`contain` range and staggered offsets, or use a different timeline per item with same range and offsets.
-3. **Use position:sticky**: Animate elements while they're stuck in position and not scrolling with the page.
-4. **@wix/interact conditions**: Include `prefers-motion` in conditions for reduced-motion users (e.g. `conditions: ['prefers-motion']`).
diff --git a/packages/interact/rules/viewenter.md b/packages/interact/rules/viewenter.md
index 5bd6291b..08422065 100644
--- a/packages/interact/rules/viewenter.md
+++ b/packages/interact/rules/viewenter.md
@@ -1,560 +1,211 @@
# ViewEnter Trigger Rules for @wix/interact
-These rules help generate viewport-based interactions using the `@wix/interact` library. ViewEnter triggers use Intersection Observer to detect when elements enter the viewport and are ideal for entrance animations, lazy loading effects, and scroll-triggered content reveals.
+This document contains rules for generating viewport-based interactions using the `@wix/interact`. ViewEnter triggers use IntersectionObserver to detect when elements enter the viewport and are ideal for entrance animations, scroll-triggered content reveals, and lazy-loading effects.
-## Rule 1: ViewEnter with Once Type for Entrance Animations
-
-**Use Case**: One-time entrance animations that play when elements first become visible (e.g., hero sections, content blocks, images, cards)
-
-**When to Apply**:
-
-- For entrance animations that should only happen once
-- When you want elements to stay in their final animated state
-- For progressive content reveal as user scrolls
-- When implementing lazy-loading visual effects
-
-**Pattern**:
-
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'viewEnter',
- params: {
- type: 'once',
- threshold: [VISIBILITY_THRESHOLD],
- inset: '[VIEWPORT_INSETS]'
- },
- effects: [
- {
- key: '[TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
- duration: [DURATION_MS],
- easing: '[EASING_FUNCTION]',
- delay: [DELAY_MS],
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[SOURCE_KEY]`: Unique identifier for element that triggers when visible (often same as target key)
-- `[TARGET_KEY]`: Unique identifier for element to animate (can be same as source or different)
-- `[VISIBILITY_THRESHOLD]`: Number between 0-1 indicating how much of element must be visible (e.g., 0.3 = 30%)
-- `[VIEWPORT_INSETS]`: String insets around viewport (e.g., '50px', '10%', '-100px')
-- `[EFFECT_TYPE]`: Either `namedEffect` or `keyframeEffect`
-- `[EFFECT_DEFINITION]`: Named effect string (e.g., 'FadeIn', 'SlideIn') or keyframe object
-- `[DURATION_MS]`: Animation duration in milliseconds (typically 500-1200ms for entrances)
-- `[EASING_FUNCTION]`: Timing function (recommended: 'ease-out', 'cubic-bezier(0.16, 1, 0.3, 1)')
-- `[DELAY_MS]`: Optional delay before animation starts
-- `[UNIQUE_EFFECT_ID]`: Optional unique identifier for animation chaining
-
-**Example - Hero Section Entrance**:
+---
-```typescript
-{
- key: 'hero-section',
- trigger: 'viewEnter',
- params: {
- type: 'once',
- threshold: 0.3,
- inset: '-100px'
- },
- effects: [
- {
- key: 'hero-section',
- keyframeEffect: {
- name: 'hero-entrance',
- keyframes: [
- { opacity: '0', transform: 'translateY(60px) scale(0.95)' },
- { opacity: '1', transform: 'translateY(0) scale(1)' }
- ]
- },
- duration: 1000,
- easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
- fill: 'backwards',
- effectId: 'hero-entrance'
- }
- ]
-}
-```
+> **CRITICAL:** When the source (trigger) and target (effect) elements are the **same element**, use ONLY `type: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`), MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing.
-**Example - Content Block Fade In**:
+## Table of Contents
-```typescript
-{
- key: 'content-block',
- trigger: 'viewEnter',
- params: {
- type: 'once',
- threshold: 0.5
- },
- effects: [
- {
- key: 'content-block',
- namedEffect: {
- type: 'FadeIn'
- },
- duration: 800,
- easing: 'ease-out',
- fill: 'backwards'
- }
- ]
-}
-```
+- [Preventing Flash of Unstyled Content (FOUC)](#preventing-flash-of-unstyled-content-fouc)
+- [Rule 1: keyframeEffect / namedEffect with ViewEnterParams](#rule-1-keyframeeffect--namedeffect-with-viewenterparams)
+- [Rule 2: customEffect with ViewEnterParams](#rule-2-customeffect-with-viewenterparams)
+- [Rule 3: Sequences with ViewEnterParams](#rule-3-sequences-with-viewenterparams)
---
-## Rule 2: ViewEnter with Repeat Type and Separate Source/Target
+## Preventing Flash of Unstyled Content (FOUC)
-**Use Case**: Animations that retrigger every time elements enter the viewport, often with separate trigger and target elements (e.g., scroll-triggered counters, image reveals, interactive sections)
+**Problem:** Elements with entrance animations (e.g. `FadeIn`) start in their final visible state (e.g. `opacity: 1`). Before the animation framework initializes and applies the starting keyframe (e.g. `opacity: 0`), the element is briefly visible at full opacity — a flash of un-animated content.
-**When to Apply**:
+**Solution:** Two things are required — **both** MUST be present for FOUC prevention to work:
-- When animations should replay on each scroll encounter
-- For scroll-triggered interactive elements
-- When using separate observer and animation targets
-- For elements that might leave and re-enter viewport
+1. **Generate critical CSS** using `generate(config)` — produces CSS rules that hide entrance-animated elements from the moment the page renders, before JavaScript runs.
+2. **Mark elements with `initial`** — set `data-interact-initial="true"` on ``, or `initial={true}` on the `` React component. This tells the runtime which elements have critical CSS applied.
-**Pattern**:
+If only one of these is present, FOUC prevention will **not** work. Both the CSS and the `initial` attribute are required.
-```typescript
-{
- key: '[OBSERVER_KEY]',
- trigger: 'viewEnter',
- params: {
- type: 'repeat',
- threshold: [VISIBILITY_THRESHOLD],
- inset: '[VIEWPORT_INSETS]'
- },
- effects: [
- {
- key: '[ANIMATION_TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
- duration: [DURATION_MS],
- easing: '[EASING_FUNCTION]',
- delay: [DELAY_MS],
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
+### Step 1: Generate CSS and inject into `` (preferred), or beginning of ``
-**Variables**:
-
-- `[OBSERVER_KEY]`: Unique identifier for element that acts as scroll trigger
-- `[ANIMATION_TARGET_KEY]`: Unique identifier for element that gets animated (different from observer)
-- Other variables same as Rule 1
-
-**Example - Image Reveal on Scroll**:
+Call `generate(config)` server-side or at build time. Inject the resulting CSS into the document `` (or in `` before your content) so it loads before the page content is painted:
```typescript
-{
- key: 'image-trigger-zone',
- trigger: 'viewEnter',
- params: {
- type: 'repeat',
- threshold: 0.1,
- inset: '-50px'
- },
- effects: [
- {
- key: 'background-image',
- keyframeEffect: {
- name: 'image-reveal',
- keyframes: [
- { filter: 'blur(20px) brightness(0.7)', transform: 'scale(1.1)' },
- { filter: 'blur(0) brightness(1)', transform: 'scale(1)' }
- ]
- },
- duration: 600,
- easing: 'ease-out',
- fill: 'backwards'
- }
- ]
-}
-```
-
-**Example - Counter Animation Repeat**:
+import { generate } from '@wix/interact';
-```typescript
-{
- key: 'stats-section',
- trigger: 'viewEnter',
- params: {
- type: 'repeat',
- threshold: 0.6
+const config: InteractConfig = {
+ interactions: [
+ {
+ key: 'hero',
+ trigger: 'viewEnter',
+ params: { type: 'once', threshold: 0.2 },
+ effects: [{ namedEffect: { type: 'FadeIn' }, duration: 800 }],
},
- effects: [
- {
- key: 'counter-display',
- customEffect: (element, progress) => {
- const targetValue = 1000;
- const currentValue = Math.floor(targetValue * progress);
- element.textContent = currentValue.toLocaleString();
- },
- duration: 2000,
- easing: 'ease-out',
- effectId: 'counter-animation'
- }
- ]
-}
-```
-
----
+ ],
+};
-## Rule 3: ViewEnter with Alternate Type and Separate Source/Target
+const css = generate(config);
+```
-**Use Case**: Animations that play forward when entering viewport and reverse when leaving, using separate observer and target elements (e.g., parallax effects, reveal/hide content, scroll-responsive UI elements)
+**Append to `` or beginning of ``:**
-**When to Apply**:
+```html
+
+```
-- For animations that should reverse when element exits viewport
-- When creating scroll-responsive reveals
-- For elements that animate in and out smoothly
-- When observer element is different from animated element
+### Step 2: Mark elements with `initial`
-**Pattern**:
+**Web (Custom Elements):**
-```typescript
-{
- key: '[OBSERVER_KEY]',
- trigger: 'viewEnter',
- params: {
- type: 'alternate',
- threshold: [VISIBILITY_THRESHOLD],
- inset: '[VIEWPORT_INSETS]'
- },
- effects: [
- {
- key: '[ANIMATION_TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
- duration: [DURATION_MS],
- easing: '[EASING_FUNCTION]',
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
+```html
+
+ ...
+
```
-**Variables**:
-Same as Rule 2
-
-**Example - Content Reveal with Hide**:
+**React:**
-```typescript
-{
- key: 'content-trigger',
- trigger: 'viewEnter',
- params: {
- type: 'alternate',
- threshold: 0.3,
- inset: '-20px'
- },
- effects: [
- {
- key: 'sidebar-content',
- keyframeEffect: {
- name: 'content-reveal-hide',
- keyframes: [
- { opacity: '0', transform: 'translateX(-50px)' },
- { opacity: '1', transform: 'translateX(0)' }
- ]
- },
- duration: 400,
- easing: 'ease-in-out',
- fill: 'backwards'
- }
- ]
-}
+```tsx
+
+ ...
+
```
-**Example - Navigation Bar Reveal**:
+**Vanilla:**
-```typescript
-{
- key: 'page-content',
- trigger: 'viewEnter',
- params: {
- type: 'alternate',
- threshold: 0.1
- },
- effects: [
- {
- key: 'floating-nav',
- keyframeEffect: {
- name: 'nav-reveal',
- keyframes: [
- { opacity: '0', transform: 'translateY(-100%)' },
- { opacity: '1', transform: 'translateY(0)' }
- ]
- },
- duration: 300,
- easing: 'ease-out',
- fill: 'backwards',
- effectId: 'nav-reveal'
- }
- ]
-}
+```html
+...
```
----
-
-## Rule 4: ViewEnter with State Type for Loop Animations
+### Rules
-**Use Case**: Looping animations that start when element enters viewport and can be paused/resumed (e.g., ambient animations, loading states, decorative effects)
+- `generate()` should be called server-side or at build time. Can also be called on the client if the page content is initially hidden (e.g. behind a loader/splash screen).
+- Only valid for `viewEnter` + `params.type: 'once'` where source and target are the same element.
+- Do NOT use for `viewEnter` with `repeat`/`alternate`/`state` types. For those, manually apply the initial keyframe as inline styles on the target element and use `fill: 'both'`.
+- If other interactions in the config also need FOUC prevention, `generate(config)` covers them all — set `initial` only on the relevant `viewEnter` + `type: 'once'` elements.
-**When to Apply**:
+## Rule 1: keyframeEffect / namedEffect with ViewEnterParams
-- For continuous animations that should start on viewport enter
-- When you need pause/resume control over scroll-triggered loops
-- For ambient or decorative animations
-- When creating scroll-activated background effects
+Use `keyframeEffect` or `namedEffect` when the viewEnter should play an animation (CSS or WAAPI). Pair with `params: ViewEnterParams` to configure the IntersectionObserver trigger.
-**Pattern**:
+**Multiple effects:** The `effects` array can contain multiple effects — all share the same viewEnter trigger and fire together when the element enters the viewport. Use this to animate different targets from a single viewport entry event.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'viewEnter',
params: {
- type: 'state',
+ type: '[VIEW_ENTER_TYPE]',
threshold: [VISIBILITY_THRESHOLD],
inset: '[VIEWPORT_INSETS]'
},
effects: [
{
key: '[TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
- duration: [DURATION_MS],
- easing: '[EASING_FUNCTION]',
- iterations: [ITERATION_COUNT],
- alternate: [ALTERNATE_BOOLEAN],
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[ITERATION_COUNT]`: Number of iterations or Infinity for continuous looping
-- `[ALTERNATE_BOOLEAN]`: true/false - whether to reverse on alternate iterations
-- Other variables same as Rule 1
-
-**Example - Floating Animation Loop**:
+ selector: '[TARGET_SELECTOR]',
-```typescript
-{
- key: 'floating-elements',
- trigger: 'viewEnter',
- params: {
- type: 'state',
- threshold: 0.4
- },
- effects: [
- {
- key: 'floating-icon',
+ // --- pick ONE of the two effect types ---
keyframeEffect: {
- name: 'floating-loop',
- keyframes: [
- { transform: 'translateY(0)' },
- { transform: 'translateY(-20px)' },
- { transform: 'translateY(0)' }
- ]
+ name: '[EFFECT_NAME]',
+ keyframes: [KEYFRAMES],
},
- duration: 3000,
- easing: 'ease-in-out',
- iterations: Infinity,
- alternate: false,
- effectId: 'floating-loop'
- }
- ]
-}
-```
+ // OR
+ namedEffect: [NAMED_EFFECT_DEFINITION],
-**Example - Breathing Light Effect**:
-
-```typescript
-{
- key: 'ambient-section',
- trigger: 'viewEnter',
- params: {
- type: 'state',
- threshold: 0.2
- },
- effects: [
- {
- key: 'light-orb',
- namedEffect: {
- type: 'Pulse'
- },
- duration: 2000,
- easing: 'ease-in-out',
- iterations: Infinity,
- alternate: true,
- effectId: 'breathing-light'
- }
- ]
-}
-```
+ fill: '[FILL_MODE]',
+ duration: [DURATION_MS],
+ easing: '[EASING_FUNCTION]',
+ delay: [DELAY_MS],
+ iterations: [ITERATIONS],
+ alternate: [ALTERNATE_BOOL],
+ effectId: '[UNIQUE_EFFECT_ID]'
+ },
+ // additional effects targeting other elements can be added here
+ ]
+}
+```
+
+### Variables
+
+- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web/vanilla, `interactKey` for React). The **source element** is observed for viewport intersection. This is the element the IntersectionObserver watches.
+- `[TARGET_KEY]` — identifier matching the element's key on the element that animates.
+- `[TARGET_SELECTOR]` - optional. Selector for the child element to select inside the root element. For `type` of `'alternate'`/`'repeat'`/`'state'` MUST either use a separate `[TARGET_KEY]` from `[SOURCE_KEY]` or `selector` for selecting a child element as target.
+- `[VIEW_ENTER_TYPE]` — `ViewEnterParams.type`. One of:
+ - `'once'` — plays once when the source element first enters the viewport and never again. Source and target may be the same element.
+ - `'repeat'` — restarts the animation every time the source element enters the viewport. Use separate source and target.
+ - `'alternate'` — plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target.
+ - `'state'` — resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`). Use separate source and target.
+- `[VISIBILITY_THRESHOLD]` — optional. Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. `0.3` = 30%).
+- `[VIEWPORT_INSETS]` — optional. String adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it).
+- `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.
+- `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`.
+- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.
+- `[FILL_MODE]` — `'backwards'` for entrance animations with `type: 'once'` (applies initial keyframe before playing). `'both'` for `'alternate'`, `'repeat'`, or `'state'` types.
+- `[DURATION_MS]` — animation duration in milliseconds.
+- `[EASING_FUNCTION]` — CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.16, 1, 0.3, 1)'`), or named easing from `@wix/motion`.
+- `[DELAY_MS]` — optional delay before the effect starts, in milliseconds.
+- `[ITERATIONS]` — optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `type: 'state'`.
+- `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration (within a single playback).
+- `[UNIQUE_EFFECT_ID]` — optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects.
---
-## Rule 5: Threshold and Viewport Intersection Parameters
+## Rule 2: customEffect with ViewEnterParams
-**Use Case**: Fine-tuning when animations trigger based on element visibility and viewport positioning (e.g., early triggers, late triggers, precise timing)
-
-**When to Apply**:
-
-- When default triggering timing isn't optimal
-- For elements that need early or late animation triggers
-- When working with very tall or very short elements
-- For precise scroll timing control
-
-**Pattern**:
+Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'viewEnter',
params: {
- type: '[BEHAVIOR_TYPE]',
- threshold: [PRECISE_THRESHOLD],
- inset: '[VIEWPORT_ADJUSTMENT]'
+ type: '[VIEW_ENTER_TYPE]',
+ threshold: [VISIBILITY_THRESHOLD],
+ inset: '[VIEWPORT_INSETS]'
},
effects: [
{
key: '[TARGET_KEY]',
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
+ customEffect: [CUSTOM_EFFECT_CALLBACK],
duration: [DURATION_MS],
- easing: '[EASING_FUNCTION]'
- }
- ]
-}
-```
-
-**Variables**:
-
-- `[PRECISE_THRESHOLD]`: Decimal between 0-1 for exact visibility percentage
-- `[VIEWPORT_ADJUSTMENT]`: Pixel or percentage adjustment to viewport detection area
-- `[BEHAVIOR_TYPE]`: 'once', 'repeat', 'alternate', or 'state'
-- Other variables same as Rule 1
-
-**Example - Early Trigger for Tall Elements**:
-
-```typescript
-{
- key: 'tall-hero-section',
- trigger: 'viewEnter',
- params: {
- type: 'once',
- threshold: 0.1, // Trigger when only 10% visible
- inset: '-200px' // Extend detection area 200px beyond viewport
- },
- effects: [
- {
- key: 'tall-hero-section',
- namedEffect: {
- type: 'SlideIn'
- },
- duration: 1200,
- easing: 'cubic-bezier(0.16, 1, 0.3, 1)'
+ easing: '[EASING_FUNCTION]',
+ effectId: '[UNIQUE_EFFECT_ID]'
}
]
}
```
-**Example - Late Trigger for Precise Timing**:
+### Variables
-```typescript
-{
- key: 'precision-content',
- trigger: 'viewEnter',
- params: {
- type: 'once',
- threshold: 0.8, // Wait until 80% visible
- inset: '50px' // Shrink detection area by 50px
- },
- effects: [
- {
- key: 'precision-content',
- keyframeEffect: {
- name: 'blur',
- keyframes: [
- { opacity: '0', filter: 'blur(5px)' },
- { opacity: '1', filter: 'blur(0)' }
- ]
- },
- duration: 600,
- easing: 'ease-out',
- fill: 'backwards'
- }
- ]
-}
-```
-
-**Example - Mobile vs Desktop Thresholds**:
-
-```typescript
-{
- conditions: {
- // Condition IDs are user-defined strings matched against these media predicates
- 'desktop-only': { type: 'media', predicate: '(min-width: 768px)' },
- },
- interactions: [
- {
- key: 'responsive-element',
- trigger: 'viewEnter',
- params: {
- type: 'once',
- threshold: 0.3,
- inset: '-100px'
- },
- conditions: ['desktop-only'],
- effects: [
- {
- key: 'responsive-element',
- namedEffect: { type: 'FadeIn' },
- duration: 800
- }
- ]
- }
- ]
-}
-```
+- `[SOURCE_KEY]` / `[TARGET_KEY]` / `[VIEW_ENTER_TYPE]` / `[VISIBILITY_THRESHOLD]` / `[VIEWPORT_INSETS]` — same as Rule 1.
+- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with `progress` from 0 to 1.
+- `[DURATION_MS]` — animation duration in milliseconds.
+- `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`.
+- `[UNIQUE_EFFECT_ID]` — optional. String identifier used for animation chaining.
---
-## Rule 6: Staggered Entrance Animations (Sequences)
-
-**Use Case**: Sequential entrance animations where multiple elements animate with staggered timing (e.g., card grids, list items, team member cards, feature sections)
-
-**When to Apply**:
+## Rule 3: Sequences with ViewEnterParams
-- When multiple elements should animate in sequence
-- For creating wave or cascade effects
-- When animating lists, grids, or collections
-- For progressive content revelation
-
-**Preferred approach: use `sequences`** on the interaction instead of manually setting `delay` on individual effects. Sequences automatically calculate stagger delays using `offset` and `offsetEasing`.
-
-**Pattern (with `listContainer`)**:
+Use sequences when a viewEnter should sync/stagger animations across multiple elements.
```typescript
{
- key: '[CONTAINER_KEY]',
+ key: '[SOURCE_KEY]',
trigger: 'viewEnter',
params: {
- type: '[BEHAVIOR_TYPE]',
- threshold: [VISIBILITY_THRESHOLD]
+ type: '[VIEW_ENTER_TYPE]',
+ threshold: [VISIBILITY_THRESHOLD],
+ inset: '[VIEWPORT_INSETS]'
},
sequences: [
{
offset: [OFFSET_MS],
offsetEasing: '[OFFSET_EASING]',
effects: [
+ // can be an inline Effect, or a reference to an effect defined in top level `effects` map
{
effectId: '[EFFECT_ID]',
listContainer: '[LIST_CONTAINER_SELECTOR]'
@@ -565,395 +216,28 @@ Same as Rule 2
}
```
-**Variables**:
-
-- `[CONTAINER_KEY]`: Unique identifier for the container element
-- `[OFFSET_MS]`: Stagger offset in ms between consecutive items (e.g., 80, 100, 120)
-- `[OFFSET_EASING]`: How the offset is distributed — `'linear'` (equal spacing), `'quadIn'` (accelerating), `'sineOut'` (decelerating), etc.
-- `[LIST_CONTAINER_SELECTOR]`: CSS selector for the list container whose children become sequence items
-- Other variables same as Rule 1
-
-**Example - Card Grid Stagger (listContainer)**:
-
-```typescript
-{
- key: 'card-grid-container',
- trigger: 'viewEnter',
- params: {
- type: 'once',
- threshold: 0.3
- },
- sequences: [
- {
- offset: 100,
- offsetEasing: 'quadIn',
- effects: [
- {
- effectId: 'card-entrance',
- listContainer: '.card-grid'
- }
- ]
- }
- ]
-}
-```
-
-With effect in the registry:
+Each `[EFFECT_ID]` must be defined in the top-level `effects` map of the `InteractConfig`:
```typescript
effects: {
- 'card-entrance': {
- duration: 500,
- easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
+ '[EFFECT_ID]': {
+ duration: [DURATION_MS],
+ easing: '[EASING_FUNCTION]',
+ fill: '[FILL_MODE]',
+ // keyframeEffect or namedEffect
keyframeEffect: {
- name: 'card-fade-up',
- keyframes: [
- { transform: 'translateY(40px)', opacity: 0 },
- { transform: 'translateY(0)', opacity: 1 }
- ]
- },
- fill: 'both'
- }
-}
-```
-
-**Example - Feature List Cascade (per-key effects)**:
-
-When items have individual keys rather than a shared container, list each as a separate effect in the sequence:
-
-```typescript
-{
- key: 'features-section',
- trigger: 'viewEnter',
- params: {
- type: 'once',
- threshold: 0.4
- },
- sequences: [
- {
- offset: 100,
- offsetEasing: 'linear',
- effects: [
- { effectId: 'feature-slide', key: 'feature-1' },
- { effectId: 'feature-slide', key: 'feature-2' },
- { effectId: 'feature-slide', key: 'feature-3' }
- ]
+ name: '[EFFECT_NAME]',
+ keyframes: [KEYFRAMES]
}
- ]
-}
-```
-
-```typescript
-effects: {
- 'feature-slide': {
- duration: 500,
- easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
- keyframeEffect: {
- name: 'feature-slide-in',
- keyframes: [
- { opacity: '0', transform: 'translateX(-30px)' },
- { opacity: '1', transform: 'translateX(0)' }
- ]
- },
- fill: 'backwards'
}
}
```
----
-
-## Advanced Patterns and Combinations
-
-### ViewEnter with Animation Chaining
-
-Using effectId to trigger subsequent animations:
-
-```typescript
-// Primary entrance
-{
- key: 'section-container',
- trigger: 'viewEnter',
- params: {
- type: 'once',
- threshold: 0.3
- },
- effects: [
- {
- key: 'section-title',
- namedEffect: {
- type: 'FadeIn'
- },
- duration: 600,
- effectId: 'title-entrance'
- }
- ]
-},
-// Chained content animation
-{
- key: 'section-title',
- trigger: 'animationEnd',
- params: {
- effectId: 'title-entrance'
- },
- effects: [
- {
- key: 'section-content',
- namedEffect: {
- type: 'SlideIn'
- },
- duration: 500,
- delay: 100
- }
- ]
-}
-```
-
-### Multi-Effect ViewEnter
-
-Animating multiple targets from single viewport trigger:
-
-```typescript
-{
- key: 'hero-trigger',
- trigger: 'viewEnter',
- params: {
- type: 'once',
- threshold: 0.2
- },
- effects: [
- {
- key: 'hero-background',
- keyframeEffect: {
- name: 'blur-bg',
- keyframes: [
- { filter: 'blur(20px)', transform: 'scale(1.1)' },
- { filter: 'blur(0)', transform: 'scale(1)' }
- ]
- },
- duration: 1200,
- easing: 'ease-out',
- fill: 'backwards'
- },
- {
- key: 'hero-title',
- namedEffect: {
- type: 'SlideIn'
- },
- duration: 800,
- delay: 300
- },
- {
- key: 'hero-subtitle',
- keyframeEffect: {
- name: 'subtitle-slide',
- keyframes: [
- { opacity: '0', transform: 'translateY(30px)' },
- { opacity: '1', transform: 'translateY(0)' }
- ]
- },
- duration: 600,
- fill: 'backwards',
- delay: 600
- },
- {
- key: 'hero-cta',
- transition: {
- duration: 400,
- delay: 900,
- styleProperties: [
- { name: 'opacity', value: '1' },
- { name: 'transform', value: 'translateY(0)' }
- ]
- }
- }
- ]
-}
-```
-
-### Conditional ViewEnter Animations
-
-Use the `conditions` config map to guard interactions by device or motion preference. Condition IDs are user-defined strings — they must be declared in the top-level `conditions` map before being referenced in an interaction.
-
-```typescript
-{
- conditions: {
- 'desktop-only': { type: 'media', predicate: '(min-width: 768px)' },
- 'prefers-motion': { type: 'media', predicate: '(prefers-reduced-motion: no-preference)' },
- 'mobile-only': { type: 'media', predicate: '(max-width: 767px)' },
- },
- interactions: [
- {
- key: 'responsive-section',
- trigger: 'viewEnter',
- params: { type: 'once', threshold: 0.5 },
- conditions: ['desktop-only', 'prefers-motion'],
- effects: [
- {
- key: 'responsive-section',
- namedEffect: { type: 'SlideIn' },
- duration: 1000
- }
- ]
- },
- // Simplified fallback for mobile or reduced-motion users
- {
- key: 'responsive-section',
- trigger: 'viewEnter',
- params: { type: 'once', threshold: 0.7 },
- conditions: ['mobile-only'],
- effects: [
- {
- key: 'responsive-section',
- namedEffect: { type: 'FadeIn' },
- duration: 400
- }
- ]
- }
- ]
-}
-```
-
----
-
-## Preventing Flash of Unstyled Content (FOUC)
-
-Use `generate(config)` from `@wix/interact/web` server-side or at build time to produce critical CSS that hides entrance elements until their animation plays.
-
-**Constraints:**
-
-- MUST be called server-side or at build time — not client-side
-- MUST set `data-interact-initial="true"` on the `` whose first child should be hidden
-- Only valid for `viewEnter` + `params.type: 'once'` where source and target are the same element
-- Do NOT use for `hover`, `click`, or `viewEnter` with `repeat`/`alternate`/`state` types
-
-```typescript
-import { generate } from '@wix/interact/web';
-
-const config: InteractConfig = {
- interactions: [
- {
- key: 'hero',
- trigger: 'viewEnter',
- params: { type: 'once', threshold: 0.2 },
- effects: [{ namedEffect: { type: 'FadeIn' }, duration: 800 }],
- },
- ],
-};
-
-// Called at build time or on the server
-const css = generate(config);
-
-// Inject into before the page renders
-const html = `
-
-
-
-
-
- ...
-
-
-`;
-```
-
----
-
-## Best Practices for ViewEnter Interactions
-
-### Behavior Guidelines
-
-1. **Use `alternate` and `repeat` types only with a separate source `key` and target `key`** to avoid re-triggering when animation starts or not triggering at all if animated target is out of viewport or clipped
-
-### Performance Guidelines
-
-1. **Use `once` type for entrance animations** to avoid repeated triggers
-2. **Be careful with separate source/target patterns** - ensure source doesn't get clipped
-3. **Use appropriate thresholds** - avoid triggering too early or too late
-
-### Threshold and Timing Guidelines
-
-1. **Use realistic thresholds** (0.1-0.5) for natural timing
-2. **Use tiny thresholds for huge elements** (0.01-0.05) for elements much larger than viewport
-3. **Provide adequate inset margins** for mobile viewports
-4. **Keep entrance animations moderate** (500-1200ms)
-5. **Use staggered delays thoughtfully** (50-200ms intervals)
-
-### Threshold and Timing Reference
-
-**Recommended Thresholds by Content Type**:
-
-- **Hero sections**: 0.1-0.3 (early trigger)
-- **Content blocks**: 0.3-0.5 (balanced trigger)
-- **Small elements**: 0.5-0.8 (late trigger)
-- **Tall sections**: 0.1-0.2 (early trigger)
-- **Huge sections**: 0.01-0.05 (ensure trigger)
-
-**Recommended Insets by Device**:
-
-- **Desktop**: '-50px' to '-200px'
-- **Mobile**: '-20px' to '-100px'
-- **Positive insets**: '50px' for precise timing
-
-### Common Use Cases by Pattern
-
-**Once Pattern**:
-
-- Hero section entrances
-- Content block reveals
-- Image lazy loading
-- Feature introductions
-- Call-to-action reveals
-
-**Repeat Pattern**:
-
-- Interactive counters
-- Scroll-triggered galleries
-- Progressive content loading
-- Repeated call-to-actions
-- Dynamic content sections
-
-**Alternate Pattern**:
-
-- Scroll-responsive UI elements
-- Reversible content reveals
-- Navigation state changes
-- Context-sensitive helpers
-- Progressive disclosure
-
-**State Pattern**:
-
-- Ambient animations
-- Background effects
-- Decorative elements
-- Loading states
-- Atmospheric content
-
-**Staggered Animations**:
-
-- Card grids and lists
-- Team member sections
-- Feature comparisons
-- Product catalogs
-- Timeline elements
-
-### Troubleshooting Common Issues
-
-**ViewEnter not triggering**:
-
-- Check if source element is clipped by parent overflow
-- Verify element exists when `Interact.create()` is called
-- Ensure threshold and inset values are appropriate
-- Check for conflicting CSS that might hide elements
-
-**ViewEnter triggering multiple times**:
-
-- Use `once` type for entrance animations
-- Avoid animating the source element if it's also the target
-- Consider using separate source and target elements
-
-**Animation performance issues**:
+### Variables
-- Limit concurrent viewEnter observers
-- Use hardware-accelerated properties
-- Avoid animating layout properties
-- Consider using `will-change` for complex animations
+- `[SOURCE_KEY]` / `[VIEW_ENTER_TYPE]` / `[VISIBILITY_THRESHOLD]` / `[VIEWPORT_INSETS]` — same as Rule 1.
+- `[OFFSET_MS]` — time offset between each child's animation start, in milliseconds.
+- `[OFFSET_EASING]` — easing curve for the stagger distribution (e.g. `'sineOut'`, `'linear'`, `'quadIn'`).
+- `[EFFECT_ID]` — string key referencing an entry in the top-level `effects` map. Same concept as `[UNIQUE_EFFECT_ID]` in Rule 1.
+- `[LIST_CONTAINER_SELECTOR]` — CSS selector for the container whose direct children will be staggered.
+- Effect definition variables (`[DURATION_MS]`, `[EASING_FUNCTION]`, `[FILL_MODE]`, `[EFFECT_NAME]`, `[KEYFRAMES]`) — same as Rule 1.
diff --git a/packages/interact/rules/viewprogress.md b/packages/interact/rules/viewprogress.md
index 6ccddb2d..493c7946 100644
--- a/packages/interact/rules/viewprogress.md
+++ b/packages/interact/rules/viewprogress.md
@@ -1,378 +1,142 @@
# ViewProgress Trigger Rules for @wix/interact
-## Core Concept
+These rules help generate scroll-driven interactions using `@wix/interact`. ViewProgress triggers create animations that update continuously as elements move through the viewport, leveraging native CSS ViewTimelines. Use when animation progress should be tied to the element's scroll position.
-`viewProgress` triggers create scroll-driven animations that update continuously as elements move through the viewport, leveraging native CSS ViewTimelines. Use when animation progress should be tied to the element's scroll position.
+> **CRITICAL:** You MUST replace all usage of `overflow: hidden` with `overflow: clip` on every element between the trigger source element and the scroll container. `overflow: hidden` creates a new scroll context that breaks the ViewTimeline; `overflow: clip` clips overflow visually without affecting scroll ancestry. If using Tailwind, replace all `overflow-hidden` classes with `overflow-clip`.
-## Config Template
+**Offset semantics:** Offset values can be a `number` representing percentages (0–100) or a `string` representing a CSS length value (e.g. `'100px'`). Positive offset values move the effective range forward along the scroll axis. 0 = start of range, 100 = end.
-```typescript
-{
- key: '[SOURCE_KEY]',
- trigger: 'viewProgress',
- conditions: ['[CONDITION_NAME]'], // optional: e.g. 'prefers-motion', 'desktop-only'
- effects: [
- {
- key: '[TARGET_KEY]',
- // Effect block — use exactly one of: namedEffect | keyframeEffect | customEffect
- namedEffect: { type: '[NAMED_EFFECT]', /* preset-specific options only if documented */ }, // OR
- keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [EFFECT_KEYFRAMES] }, // OR
- customEffect: (element, progress) => { [CUSTOM_LOGIC] },
- rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
- rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
- easing: '[EASING_FUNCTION]',
- fill: '[FILL_MODE]',
- effectId: '[UNIQUE_EFFECT_ID]'
- }
- ]
-}
-```
-
-## Variable Key
-
-| Placeholder | Valid Values / Notes |
-| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `[SOURCE_KEY]` | Unique identifier for element that tracks scroll progress |
-| `[TARGET_KEY]` | Unique identifier for element to animate (can equal source) |
-| `[NAMED_EFFECT]` | Preset from @wix/motion-presets (see Named Scroll Effects below). Some presets accept options (e.g. `direction`) — only use options you have documentation for; omit and rely on defaults otherwise |
-| `[EFFECT_NAME]` | Unique name for keyframe effect |
-| `[EFFECT_KEYFRAMES]` | Array of keyframe objects, e.g. `[{ opacity: '0' }, { opacity: '1' }]` |
-| `[CUSTOM_LOGIC]` | JS: `progress` is 0–1 within range; mutate `element.style` or DOM |
-| `[RANGE_NAME]` | 'cover', 'contain', 'entry', 'exit', 'entry-crossing', 'exit-crossing' |
-| `[START_PERCENTAGE]` | 0–100 |
-| `[END_PERCENTAGE]` | 0–100 |
-| `[EASING_FUNCTION]` | 'linear', 'ease-in', 'ease-out', 'ease-in-out', or cubic-bezier string |
-| `[FILL_MODE]` | 'both', 'backwards', 'forwards', 'none' |
-| `[UNIQUE_EFFECT_ID]` | Optional unique identifier |
-| `[CONDITION_NAME]` | User-defined condition ID declared in the top-level `conditions` map (e.g. `'prefers-motion'`, `'desktop-only'`) |
-
-**Offset semantics:** Positive offset values move the effective range forward along the scroll axis. 0 = start of range, 100 = end.
-
-## Effect Type Selection
+## Table of Contents
-| Scenario | Effect Type | Notes |
-| ------------------------------------------------------------- | ---------------- | --------------------------------- |
-| Parallax, scroll-responsive decorations, floating elements | `namedEffect` | Use presets; fastest to implement |
-| Custom multi-property animations, brand-specific reveals | `keyframeEffect` | Full control over CSS keyframes |
-| Dynamic content (counters, text reveal, canvas, calculations) | `customEffect` | JS callback; `progress` 0–1 |
+- [Rule 1: ViewProgress with keyframeEffect or namedEffect](#rule-1-viewprogress-with-keyframeeffect-or-namedeffect)
+- [Rule 2: ViewProgress with customEffect](#rule-2-viewprogress-with-customeffect)
+- [Rule 3: ViewProgress with Tall Wrapper + Sticky Container (contain range)](#rule-3-viewprogress-with-tall-wrapper--sticky-container-contain-range)
-## Range Reference
+---
-| Intent | rangeStart.name | rangeEnd.name | Typical Offsets |
-| --------------------------------------- | --------------- | ------------- | ---------------------- |
-| Parallax / continuous while visible | cover | cover | 0–100 |
-| Entry animation (element entering view) | entry | entry | 0–30 start, 70–100 end |
-| Exit animation (element leaving view) | exit | exit | 0–30 start, 70–100 end |
-| Cross-range (entry to exit) | entry | exit | 0–100 |
-| Contained phase | contain | contain | 0–100 |
+## Rule 1: ViewProgress with keyframeEffect or namedEffect
-## Named Scroll Effects
+**Use Case**: Scroll-driven CSS-based effects.
-From `@wix/motion-presets` scroll animations: ParallaxScroll, MoveScroll, FadeScroll, RevealScroll, GrowScroll, SlideScroll, SpinScroll, PanScroll, BlurScroll, ArcScroll, FlipScroll, Spin3dScroll, TiltScroll, TurnScroll, ShapeScroll, ShuttersScroll, ShrinkScroll, SkewPanScroll, StretchScroll.
+**Multiple effects:** The `effects` array can contain multiple effects — all are driven by the same scroll progress. Use this to animate different targets or properties in sync with the same scroll position.
-## Examples
-
-### Example 1: Named Effect (Parallax)
+### Template
```typescript
{
- key: 'hero-section',
+ key: '[SOURCE_KEY]',
trigger: 'viewProgress',
effects: [
{
- key: 'hero-background',
- namedEffect: {
- type: 'ParallaxScroll'
- },
- rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear'
- }
- ]
-}
-```
-
-### Example 2: Keyframe Effect (Custom Animation)
+ key: '[TARGET_KEY]',
+ // --- pick ONE of the two effect types ---
+ namedEffect: [NAMED_EFFECT_DEFINITION],
+ // OR
+ keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [EFFECT_KEYFRAMES] },
-```typescript
-{
- key: 'card-section',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'product-card',
- keyframeEffect: {
- name: 'card-entrance',
- keyframes: [
- { opacity: '0', transform: 'translateY(80px) scale(0.9)', filter: 'blur(5px)' },
- { opacity: '1', transform: 'translateY(0) scale(1)', filter: 'blur(0)' }
- ]
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 70 } },
- easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
- fill: 'both'
- }
+ rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
+ rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
+ easing: '[EASING_FUNCTION]',
+ fill: 'both',
+ effectId: '[UNIQUE_EFFECT_ID]'
+ },
+ // additional effects targeting other elements can be added here
]
}
```
-### Example 3: Custom Effect (Dynamic Content)
+### Variables
-```typescript
-{
- key: 'text-section',
- trigger: 'viewProgress',
- effects: [
- {
- key: 'animated-text',
- customEffect: (element, progress) => {
- const text = element.dataset.fullText || element.textContent;
- const visibleLength = Math.floor(text.length * progress);
- const visibleText = text.substring(0, visibleLength);
- element.textContent = visibleText + (progress < 1 ? '|' : '');
+- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). The element whose scroll position drives the animation.
+- `[TARGET_KEY]` — identifier matching the element's key on the element to animate (can be same as source or different).
+- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. **CRITICAL:** Scroll presets (`*Scroll`) MUST include `range: 'in' | 'out' | 'continuous'` in their options. `'in'` ends at the idle state, `'out'` starts from the idle state, `'continuous'` passes through it.
+- `[EFFECT_NAME]` — unique name for custom keyframe effect.
+- `[EFFECT_KEYFRAMES]` — array of keyframe objects defining CSS property values (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.
+- `[RANGE_NAME]` — scroll range name:
+ - `'cover'` — full visibility span from first pixel entering to last pixel leaving.
+ - `'entry'` — the phase while the element is entering the viewport.
+ - `'exit'` — the phase while the element is exiting the viewport.
+ - `'contain'` — while the element is fully contained in the viewport. Typically used with a `position: sticky` container.
+- `[START_PERCENTAGE]` / `[END_PERCENTAGE]` — 0–100, sub-range within the named range.
+- `[EASING_FUNCTION]` — typically `'linear'` for scroll effects; non-linear easing can feel jarring as scroll position changes.
+- `[UNIQUE_EFFECT_ID]` — optional identifier for referencing the effect externally.
- element.style.opacity = Math.min(1, progress * 2);
- element.style.transform = `translateY(${(1 - progress) * 30}px)`;
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 80 } },
- fill: 'both',
- effectId: 'text-reveal'
- }
- ]
-}
-```
+---
+
+## Rule 2: ViewProgress with customEffect
-### Example 4: Multi-Range (Entry + Exit on the same element)
+**Use Case**: Scroll-driven effects requiring JavaScript logic (e.g., changing SVG attributes, controlling WebGL/WebGPU effects).
-Animating the same element in on scroll entry and out on scroll exit requires two separate effects within the same interaction — one scoped to the `entry` range, one to `exit`. This pattern is non-obvious because both effects share the same `key` but have different ranges.
+### Template
```typescript
{
- key: 'feature-card',
+ key: '[SOURCE_KEY]',
trigger: 'viewProgress',
+ conditions: ['[CONDITION_NAME]'], // optional
effects: [
- // Animate IN as element enters viewport
{
- key: 'feature-card',
- keyframeEffect: {
- name: 'card-in',
- keyframes: [
- { opacity: '0', transform: 'translateY(40px)' },
- { opacity: '1', transform: 'translateY(0)' }
- ]
+ key: '[TARGET_KEY]',
+ customEffect: (element: Element, progress: number) => {
+ // progress is 0–1 within the specified range
+ [CUSTOM_LOGIC]
},
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 60 } },
- easing: 'ease-out',
- fill: 'both'
+ rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
+ rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
+ fill: 'both',
+ effectId: '[UNIQUE_EFFECT_ID]'
},
- // Animate OUT as element exits viewport
- {
- key: 'feature-card',
- keyframeEffect: {
- name: 'card-out',
- keyframes: [
- { opacity: '1', transform: 'translateY(0)' },
- { opacity: '0', transform: 'translateY(-40px)' }
- ]
- },
- rangeStart: { name: 'exit', offset: { unit: 'percentage', value: 40 } },
- rangeEnd: { name: 'exit', offset: { unit: 'percentage', value: 100 } },
- easing: 'ease-in',
- fill: 'both'
- }
+ // additional effects targeting other elements can be added here
]
}
```
-## Advanced Patterns
-
-### Multi-Range ViewProgress Effects
+### Variables
-Combining different ranges for complex scroll animations:
+- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1.
+- `[CUSTOM_LOGIC]` — JavaScript that uses `progress` (0–1) and the target `element` to apply the effect. Avoid layout/style reads inside the callback for smooth performance.
+- `[RANGE_NAME]` / `[START_PERCENTAGE]` / `[END_PERCENTAGE]` — same as Rule 1.
+- `[UNIQUE_EFFECT_ID]` — optional identifier for referencing the effect externally.
-```typescript
-{
- key: 'complex-section',
- trigger: 'viewProgress',
- effects: [
- // Entry phase
- {
- key: 'section-content',
- keyframeEffect: {
- name: 'content-entrance',
- keyframes: [
- { opacity: '0', transform: 'translateY(50px)' },
- { opacity: '1', transform: 'translateY(0)' }
- ]
- },
- rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 50 } },
- easing: 'ease-out',
- fill: 'backwards'
- },
- // Cover phase
- {
- key: 'background-element',
- keyframeEffect: {
- name: 'background-parallax-hue',
- keyframes: [
- { transform: 'translateY(0)', filter: 'hue-rotate(0deg)' },
- { transform: 'translateY(-100px)', filter: 'hue-rotate(180deg)' }
- ]
- },
- rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear',
- fill: 'both'
- },
- // Exit phase
- {
- key: 'section-content',
- keyframeEffect: {
- name: 'content-exit',
- keyframes: [
- { opacity: '1', transform: 'scale(1)' },
- { opacity: '0', transform: 'scale(0.8)' }
- ]
- },
- rangeStart: { name: 'exit', offset: { unit: 'percentage', value: 50 } },
- rangeEnd: { name: 'exit', offset: { unit: 'percentage', value: 100 } },
- easing: 'ease-in',
- fill: 'forwards'
- }
- ]
-}
-```
+---
-### ViewProgress with Conditional Behavior
+## Rule 3: ViewProgress with Tall Wrapper + Sticky Container (contain range)
-Use interact `conditions` for responsive scroll animations and `prefers-reduced-motion`. Condition IDs are user-defined strings — they must be declared in the top-level `conditions` map before being referenced in an interaction.
+**Use Case**: Scroll-driven animations inside a sticky-positioned container, where the source element is a tall wrapper and the effect applies during the "stuck" phase using `position: sticky` to lock a container and `contain` range to animate only during the stuck phase. Good for heavy effects on large media elements or scrolly-telling effects.
-```typescript
-{
- conditions: {
- 'desktop-only': { type: 'media', predicate: '(min-width: 768px)' },
- 'prefers-motion': { type: 'media', predicate: '(prefers-reduced-motion: no-preference)' },
- 'mobile-only': { type: 'media', predicate: '(max-width: 767px)' },
- },
- interactions: [
- {
- key: 'responsive-parallax',
- trigger: 'viewProgress',
- conditions: ['desktop-only', 'prefers-motion'],
- effects: [
- {
- key: 'parallax-bg',
- keyframeEffect: {
- name: 'parallax-bg',
- keyframes: [
- { transform: 'translateY(0)' },
- { transform: 'translateY(-300px)' }
- ]
- },
- rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear',
- fill: 'both'
- }
- ]
- },
- // Simplified fallback for mobile
- {
- key: 'responsive-parallax',
- trigger: 'viewProgress',
- conditions: ['mobile-only'],
- effects: [
- {
- key: 'parallax-bg',
- keyframeEffect: {
- name: 'fade-out-bg',
- keyframes: [
- { opacity: '1' },
- { opacity: '0.7' }
- ]
- },
- rangeStart: { name: 'exit', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'exit', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear',
- fill: 'both'
- }
- ]
- }
- ]
-}
-```
+**Layout Structure**:
-### Multiple Element Coordination
+- **Tall wrapper** (`[TALL_WRAPPER_KEY]`): An element with enough height to create scroll distance (e.g., `height: 300vh`). This is the ViewTimeline source. The taller it is relative to the viewport, the longer the scroll distance and the more "duration" the animation has.
+- **Sticky container**: A direct child with `position: sticky; top: 0; height: 100vh` that stays fixed in the viewport while the wrapper scrolls past.
+- **Animated elements** (`[STICKY_CHILD_KEY]`): Children of the sticky container that receive the effects.
-Orchestrating multiple elements with viewProgress:
+### Template
```typescript
{
- key: 'orchestrated-section',
+ key: '[TALL_WRAPPER_KEY]',
trigger: 'viewProgress',
+ conditions: ['[CONDITION_NAME]'], // optional
effects: [
{
- key: 'bg-layer-1',
- keyframeEffect: {
- name: 'layer-1-parallax',
- keyframes: [
- { transform: 'translateY(0)' },
- { transform: 'translateY(-50px)' }
- ]
- },
- rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear',
- fill: 'both'
- },
- {
- key: 'bg-layer-2',
- keyframeEffect: {
- name: 'layer-2-parallax',
- keyframes: [
- { transform: 'translateY(0)' },
- { transform: 'translateY(-100px)' }
- ]
- },
- rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear',
- fill: 'both'
+ key: '[STICKY_CHILD_KEY]',
+ // Use keyframeEffect, namedEffect, or customEffect as in Rules 1–2
+ keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [EFFECT_KEYFRAMES] },
+ rangeStart: { name: 'contain', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
+ rangeEnd: { name: 'contain', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
+ easing: '[EASING_FUNCTION]',
+ fill: 'both',
+ effectId: '[UNIQUE_EFFECT_ID]'
},
- {
- key: 'fg-content',
- keyframeEffect: {
- name: 'layer-3-parallax',
- keyframes: [
- { transform: 'translateY(0)' },
- { transform: 'translateY(-150px)' }
- ]
- },
- rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 0 } },
- rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 100 } },
- easing: 'linear',
- fill: 'both'
- }
+ // additional effects targeting other elements can be added here
]
}
```
-## Best Practices
-
-### Interact-Specific
-
-1. **Respect `prefers-reduced-motion`** via interact `conditions`: use `'prefers-motion'` so scroll animations run only when the user has not requested reduced motion.
-2. **Use `linear` easing** for most scroll effects; non-linear easing can feel jarring as scroll position changes.
-3. **Range configuration:** Verify source element remains visible throughout the scroll range. If the source is hidden or in a frozen stacking context, the ViewTimeline constraint may not update correctly.
-4. **Avoid overlapping ranges** on the same target to prevent conflicting animations.
-5. **Entry/exit timing:** Use 0–50% cover or 0–100% entry for entrances; 50–100% cover or 0–100% exit for exits. Start with broad ranges (0–100) then refine.
-6. **customEffect:** Use `element.closest('interact-element')` when querying related DOM within the callback; target elements must exist when the effect runs.
-
-### Troubleshooting
+### Variables
-- **Unexpected behavior:** Check range names match intent; verify source visibility; ensure target elements exist.
-- **Janky custom effects:** Simplify calculations; avoid layout-triggering reads in the callback.
+- `[TALL_WRAPPER_KEY]` — key for the tall outer element that defines the scroll distance — this is the ViewTimeline source.
+- `[STICKY_CHILD_KEY]` — key for the animated element inside the sticky container.
+- `[EFFECT_NAME]` / `[EFFECT_KEYFRAMES]` — same as Rule 1.
+- `[START_PERCENTAGE]` / `[END_PERCENTAGE]` — 0–100 within the `contain` range, i.e. the phase where the sticky container is fully stuck.
+- `[EASING_FUNCTION]` / `[UNIQUE_EFFECT_ID]` — same as Rule 1.
diff --git a/packages/interact/src/types.ts b/packages/interact/src/types.ts
index d04e8857..03ea970d 100644
--- a/packages/interact/src/types.ts
+++ b/packages/interact/src/types.ts
@@ -109,6 +109,7 @@ export type TimeEffect = {
fill?: Fill;
reversed?: boolean;
delay?: number;
+ composite?: CompositeOperation;
} & EffectEffectProperty;
export type ScrubEffect = {
@@ -117,6 +118,7 @@ export type ScrubEffect = {
alternate?: boolean;
fill?: Fill;
reversed?: boolean;
+ composite?: CompositeOperation;
rangeStart?: RangeOffset;
rangeEnd?: RangeOffset;
centeredToTarget?: boolean;