From 2d39973db4e4e12ce247f01ac2380109c5e646f1 Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Sun, 15 Mar 2026 12:15:19 +0200 Subject: [PATCH 01/13] Revisiti click and hover ruels --- packages/interact/rules/click.md | 643 ++++--------------------------- packages/interact/rules/hover.md | 616 +++++------------------------ 2 files changed, 174 insertions(+), 1085 deletions(-) diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md index 17805bd..6367123 100644 --- a/packages/interact/rules/click.md +++ b/packages/interact/rules/click.md @@ -1,137 +1,42 @@ # 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 +**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) +--- -**When to Apply**: +## Rule 1: keyframeEffect / namedEffect with PointerTriggerParams -- 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 +Use `keyframeEffect` or `namedEffect` when the click should play an animation (CSS or WAAPI). Pair with `PointerTriggerParams` to control playback behavior. -**Pattern**: +Always include `fill: 'both'` so the effect remains applied while finished and is not garbage-collected, allowing it to be efficiently toggled. ```typescript { key: '[SOURCE_KEY]', trigger: 'click', params: { - type: 'alternate' + type: '[POINTER_TYPE]' }, 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', + // --- pick ONE of the two effect types --- keyframeEffect: { - name: 'accordion', - keyframes: [ - { clipPath: 'inset(0 0 100% 0)', opacity: '0' }, - { clipPath: 'inset(0 0 0 0)', opacity: '1' } - ] + name: '[EFFECT_NAME]', + keyframes: [KEYFRAMES], }, - fill: 'both', - reversed: true, - duration: 400, - easing: 'ease-in-out' - } - ] -} -``` - ---- - -## Rule 2: Click with TimeEffect and State Pattern + // OR + namedEffect: { type: '[NAMED_EFFECT_TYPE]' }, -**Use Case**: Animations that can be paused and resumed with clicks (e.g., video controls, loading animations, slideshow controls) - -**When to Apply**: - -- When you need play/pause functionality -- For controlling ongoing animations -- When users should be able to interrupt and resume animations -- For interactive media controls - -**Pattern**: - -```typescript -{ - key: '[SOURCE_KEY]', - trigger: 'click', - params: { - type: 'state' - }, - effects: [ - { - key: '[TARGET_KEY]', - [EFFECT_TYPE]: [EFFECT_DEFINITION], fill: 'both', reversed: [INITIAL_REVERSED_BOOL], duration: [DURATION_MS], easing: '[EASING_FUNCTION]', - iterations: [ITERATION_COUNT], + delay: [DELAY_MS], + iterations: [ITERATIONS], alternate: [ALTERNATE_BOOL], effectId: '[UNIQUE_EFFECT_ID]' } @@ -139,515 +44,133 @@ These rules help generate click-based interactions using the `@wix/interact` lib } ``` -**Variables**: +### 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', - keyframeEffect: { - name: 'spin', - keyframes: [ - { transform: 'rotate(0deg)' }, - { transform: 'rotate(360deg)' } - ] - }, - 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' - } - ] -} -``` +- `[SOURCE_KEY]` — identifier matching the `data-interact-key` attribute on the element that listens for clicks. +- `[TARGET_KEY]` — identifier matching the `data-interact-key` attribute on the element that animates. Same as `[SOURCE_KEY]` for self-targeting, or different for cross-targeting. +- `[POINTER_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'` — pauses/resumes the animation on each click. Useful for continuous loops (`iterations: Infinity`). +- `[KEYFRAMES]` — WAAPI-style keyframes format as array of keyframe objects or object of properties to arrays of values. +- `[EFFECT_NAME]` — arbitrary string identifier for a `keyframeEffect`. +- `[NAMED_EFFECT_TYPE]` — pre-built effect from `@wix/motion-presets` (e.g. `'FadeIn'`, `'SlideIn'`, `'Pulse'`, `'Breathe'`). +- `[INITIAL_REVERSED_BOOL]` — optional. `true` to start in the "played" state so the first click reverses the animation. +- `[DURATION_MS]` — animation duration in milliseconds. Typical click range: 100–500. +- `[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 (pair with `type: 'state'`). +- `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration. +- `[UNIQUE_EFFECT_ID]` — optional. String identifier used for animation chaining or sequence references. --- -## Rule 3: Click with TimeEffect and Repeat Pattern - -**Use Case**: Animations that restart from the beginning each time clicked (e.g., pulse effects, notification badges, emphasis animations) +## Rule 2: transition / transitionProperties with StateParams -**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**: +Use `transition` or `transitionProperties` when the click should toggle CSS property values via CSS transitions rather than keyframe animations. Pair with `StateParams` to control how the style is applied. ```typescript { key: '[SOURCE_KEY]', trigger: 'click', params: { - type: 'repeat' + method: '[TRANSITION_METHOD]' }, effects: [ { key: '[TARGET_KEY]', - [EFFECT_TYPE]: [EFFECT_DEFINITION], - duration: [DURATION_MS], - easing: '[EASING_FUNCTION]', - delay: [DELAY_MS], - 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' - } - ] -} -``` - -**Example - Success Notification**: - -```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' - } - ] -} -``` - ---- - -## 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) - -**When to Apply**: - -- 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**: - -```typescript -{ - key: '[SOURCE_KEY]', - trigger: 'click', - params: { - method: 'toggle' // also: 'add', 'remove', 'clear' — see full-lean.md StateParams - }, - 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]' } ] }, - effectId: '[UNIQUE_EFFECT_ID]' - } - ] -} -``` - -**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 - -**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' + // OR (when each property needs its own timing) + transitionProperties: [ + { + name: '[CSS_PROP]', + value: '[VALUE]', + duration: [DURATION_MS], + delay: [DELAY_MS], + easing: '[EASING_FUNCTION]' + } + ] } ] } ``` -**Example - Button Style Toggle**: - -```typescript -{ - key: 'style-toggle', - trigger: 'click', - params: { - method: 'toggle' - }, - 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)' } - ] - } - } - ] -} -``` +### Variables -**Example - Card State Toggle**: +- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. +- `[TRANSITION_METHOD]` — `StateParams.method`. One of: + - `'add'` — adds the state on click. + - `'remove'` — removes the state on click. + - `'toggle'` — toggles the state on each click. Default. + - `'clear'` — clears all previously applied states on click. +- `[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`. -```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)' } - ] - } - } - ] -} -``` +Use `transition` when all properties share timing. Use `transitionProperties` when each property needs independent `duration`, `delay`, or `easing`. --- -## Rule 5: Click with Sequence (Staggered Multi-Element Orchestration) - -**Use Case**: Click-triggered coordinated animations across multiple elements with staggered timing (e.g., page section reveals, multi-element toggles, orchestrated content entrances) +## Rule 3: Sequences -**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: '[POINTER_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]' } + { + 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' } - ] - } - ] -} -``` +The referenced `effectId` 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: { - 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]` — same as Rule 1. +- `[POINTER_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. +- `[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/hover.md b/packages/interact/rules/hover.md index 8eb0cbd..3902f99 100644 --- a/packages/interact/rules/hover.md +++ b/packages/interact/rules/hover.md @@ -1,507 +1,142 @@ # 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 +**Accessible hover**: Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus. -**Purpose**: Generate basic hover interactions with enter/leave animations +**Hit-area shift**: When a hover effect changes the **size or position** of the hovered element (e.g. `transform: scale(…)`, `translateX(…)`, width/height changes), the element's hit-area shifts with it. This causes rapid enter/leave events and visual flickering. -**Pattern**: +To avoid this, use a **separate source and target**: -```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 - -**Default Values**: - -- `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' - } - ] -} +- `key` (source) — a stable wrapper element that receives the pointer events. +- Effect `key` or `selector` (target) — the other/inner element that actually animates. -// 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 2: Hover Alternate Animations (namedEffect / keyframeEffect) +## Rule 1: keyframeEffect / namedEffect with PointerTriggerParams -**Purpose**: Hover interactions that play forward on mouse enter and reverse on mouse leave (`type: 'alternate'`). +Use `keyframeEffect` or `namedEffect` when the hover should play a an animation (CSS or WAAPI). Pair with `PointerTriggerParams` to control playback behavior. -**Pattern**: +Always include `fill: 'forwards'` or `fill: 'both'` so the effect remains applied while hovering. ```typescript { key: '[SOURCE_KEY]', trigger: 'hover', params: { - type: 'alternate' + type: '[POINTER_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: { type: '[NAMED_EFFECT_TYPE]' }, -```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] } ] } ``` -**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 `data-interact-key` attribute on the element that listens for hover. +- `[TARGET_KEY]` — identifier matching the `data-interact-key` attribute on the element that animates. Same as `[SOURCE_KEY]` for self-targeting, or different when source and target must be separated (see above). +- `[POINTER_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. Pause and rewind on leave. + - `'once'` — plays once on the first enter and never again. + - `'state'` — pauses/resumes the animation on enter/leave. Useful for continuous loops (`iterations: Infinity`). +- `[KEYFRAMES]` - WAAPI-style keyframes format as array of keyframe objects or object of properties to arrays of values. +- `[EFFECT_NAME]` — arbitrary string identifier for a `keyframeEffect`. +- `[NAMED_EFFECT_TYPE]` — pre-built effect from `@wix/motion-presets` (e.g. `'FadeIn'`, `'SlideIn'`, `'Pulse'`, `'Breathe'`). +- `[FILL_MODE]` — usually `'both'`. Keeps the final state applied while hovering, and prevents GC of animation when finished. +- `[DURATION_MS]` — animation duration in milliseconds. Typical hover range: 150–400. +- `[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 (pair with `type: 'state'`). +- `[ALTERNATE_BOOL]` - optional. `true` to alternate direction on every other iteration. -- `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 +--- -**Purpose**: Generate hover interactions that pause/resume on hover (state-based control) +## Rule 2: transition / transitionProperties with StateParams -**Pattern**: +Use `transition` or `transitionProperties` when the hover should toggle CSS property values via CSS transitions rather than keyframe animations. Pair with `StateParams` to control how the style is applied. ```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]' } ] }, - 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] - }, - { - key: '[TARGET_2]', - [EFFECT_TYPE]: [EFFECT_DEFINITION_2], - fill: [FILL_2], - reversed: [REVERSED_BOOL_2], - duration: [DURATION_2], - delay: [DELAY_2] + // OR (when each property needs its own timing) + transitionProperties: [ + { + name: '[CSS_PROP]', + value: '[VALUE]', + duration: [DURATION_MS], + delay: [DELAY_MS], + easing: '[EASING_FUNCTION]' + } + ] } ] } ``` -**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. - -**Use Cases**: - -- Card hover affecting image, text, and button -- Navigation item hover affecting icon and text -- Complex component state changes - -**Timing Strategies**: +### Variables -- Simultaneous: All delays = 0 -- Staggered: Incrementing delays (0, 50, 100ms) -- Sequential: Non-overlapping delays +- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. +- `[TRANSITION_METHOD]` — `StateParams.method`. One of: + - `'add'` — adds the state on enter. + - `'remove'` — removes the state on enter. + - `'toggle'` — adds the state on enter, removes on leave. Default. + - `'clear'` — clears all previously applied states on enter. +- `[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 Generations**: +Use `transition` when all properties share timing. Use `transitionProperties` when each property needs independent `duration`, `delay`, or `easing`. -```typescript -// Product card with multiple targets -{ - key: 'product-card', - trigger: 'hover', - params: { - type: 'alternate' - }, - 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: '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 - } - ] -} -``` - -## Rule 6: Hover with Sequence (Staggered Multi-Target) - -**Purpose**: Hover interactions that stagger animations across multiple targets using a sequence instead of manual delays. - -**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 3: Sequences -**Pattern**: +Use sequences when a hover should sync/stagger animations across multiple elements. ```typescript { key: '[SOURCE_KEY]', trigger: 'hover', params: { - type: 'repeat' + type: '[POINTER_TYPE]' }, sequences: [ { @@ -518,97 +153,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' - } - ] - } - ] -} -``` +The referenced `effectId` 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: { - 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]` — same as Rule 1. +- `[POINTER_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. +- `[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. From 67f6708a1d65abeee33316ea97e812e2353d964e Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Sun, 15 Mar 2026 12:21:46 +0200 Subject: [PATCH 02/13] Remove old plan --- .../interact/rules/MASTER-CLEANUP-PLAN.md | 286 ------------------ 1 file changed, 286 deletions(-) delete mode 100644 packages/interact/rules/MASTER-CLEANUP-PLAN.md diff --git a/packages/interact/rules/MASTER-CLEANUP-PLAN.md b/packages/interact/rules/MASTER-CLEANUP-PLAN.md deleted file mode 100644 index dc9c9fb..0000000 --- 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. From 5d8c3b98590c12103b2bc2752ee0a5f43ad92df7 Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Sun, 15 Mar 2026 12:23:12 +0200 Subject: [PATCH 03/13] Purge old plans --- .../plans/motion_presets_llm_rules.plan.md | 729 ------------------ .../sequence_docs_and_demos_9e654633.plan.md | 346 --------- ...ce_feature_implementation_84c01c97.plan.md | 317 -------- ...ence_removegroups_support_9b4d0693.plan.md | 266 ------- .../sequences_feature_tests_e12d5b15.plan.md | 211 ----- 5 files changed, 1869 deletions(-) delete mode 100644 .cursor/plans/motion_presets_llm_rules.plan.md delete mode 100644 .cursor/plans/sequence_docs_and_demos_9e654633.plan.md delete mode 100644 .cursor/plans/sequence_feature_implementation_84c01c97.plan.md delete mode 100644 .cursor/plans/sequence_removegroups_support_9b4d0693.plan.md delete mode 100644 .cursor/plans/sequences_feature_tests_e12d5b15.plan.md 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 7ce5c8e..0000000 --- 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 4575812..0000000 --- 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 6bf4593..0000000 --- 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 e4a81dd..0000000 --- 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 ece558d..0000000 --- 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 From 83bb5b1a97cfde2abb83f48802249e348edd2bb0 Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Sun, 15 Mar 2026 12:23:42 +0200 Subject: [PATCH 04/13] Purge old spec --- packages/interact/dev/sequences-spec.md | 176 ------------------------ 1 file changed, 176 deletions(-) delete mode 100644 packages/interact/dev/sequences-spec.md diff --git a/packages/interact/dev/sequences-spec.md b/packages/interact/dev/sequences-spec.md deleted file mode 100644 index 6d8e455..0000000 --- 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: [{ - - }] - }] - }] -} -``` From a70ea9b58243bba05af83cf9689efb1c15afa387 Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Sun, 15 Mar 2026 16:56:18 +0200 Subject: [PATCH 05/13] Small fixes --- packages/interact/rules/click.md | 3 ++- packages/interact/rules/hover.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md index 6367123..a026fed 100644 --- a/packages/interact/rules/click.md +++ b/packages/interact/rules/click.md @@ -31,7 +31,7 @@ Always include `fill: 'both'` so the effect remains applied while finished and i // OR namedEffect: { type: '[NAMED_EFFECT_TYPE]' }, - fill: 'both', + fill: '[FILL_MODE]', reversed: [INITIAL_REVERSED_BOOL], duration: [DURATION_MS], easing: '[EASING_FUNCTION]', @@ -57,6 +57,7 @@ Always include `fill: 'both'` so the effect remains applied while finished and i - `[EFFECT_NAME]` — arbitrary string identifier for a `keyframeEffect`. - `[NAMED_EFFECT_TYPE]` — pre-built effect from `@wix/motion-presets` (e.g. `'FadeIn'`, `'SlideIn'`, `'Pulse'`, `'Breathe'`). - `[INITIAL_REVERSED_BOOL]` — optional. `true` to start in the "played" state so the first click reverses the animation. +- `[FILL_MODE]` — usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished. - `[DURATION_MS]` — animation duration in milliseconds. Typical click range: 100–500. - `[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. diff --git a/packages/interact/rules/hover.md b/packages/interact/rules/hover.md index 3902f99..30b1d2c 100644 --- a/packages/interact/rules/hover.md +++ b/packages/interact/rules/hover.md @@ -15,7 +15,7 @@ To avoid this, use a **separate source and target**: ## Rule 1: keyframeEffect / namedEffect with PointerTriggerParams -Use `keyframeEffect` or `namedEffect` when the hover should play a an animation (CSS or WAAPI). Pair with `PointerTriggerParams` to control playback behavior. +Use `keyframeEffect` or `namedEffect` when the hover should play an animation (CSS or WAAPI). Pair with `PointerTriggerParams` to control playback behavior. Always include `fill: 'forwards'` or `fill: 'both'` so the effect remains applied while hovering. @@ -61,7 +61,7 @@ Always include `fill: 'forwards'` or `fill: 'both'` so the effect remains applie - `[KEYFRAMES]` - WAAPI-style keyframes format as array of keyframe objects or object of properties to arrays of values. - `[EFFECT_NAME]` — arbitrary string identifier for a `keyframeEffect`. - `[NAMED_EFFECT_TYPE]` — pre-built effect from `@wix/motion-presets` (e.g. `'FadeIn'`, `'SlideIn'`, `'Pulse'`, `'Breathe'`). -- `[FILL_MODE]` — usually `'both'`. Keeps the final state applied while hovering, and prevents GC of animation when finished. +- `[FILL_MODE]` — usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished. - `[DURATION_MS]` — animation duration in milliseconds. Typical hover range: 150–400. - `[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. From 74a2bf275fe93190403d7730f7ba9c63e4d050dd Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Sun, 15 Mar 2026 18:55:40 +0200 Subject: [PATCH 06/13] Revisit viewprogress rules --- packages/interact/rules/viewprogress.md | 391 +++++------------------- 1 file changed, 73 insertions(+), 318 deletions(-) diff --git a/packages/interact/rules/viewprogress.md b/packages/interact/rules/viewprogress.md index 6ccddb2..50deb9f 100644 --- a/packages/interact/rules/viewprogress.md +++ b/packages/interact/rules/viewprogress.md @@ -1,23 +1,34 @@ # ViewProgress Trigger Rules for @wix/interact -## Core Concept +These rules help generate scroll-driven interactions using the `@wix/interact` library. 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. +> **IMPORTANT:** 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. -## Config Template +**Offset semantics:** Values can be as a `string` representing CSS value, or `number` representing percentages. Positive offset values move the effective range forward along the scroll axis. 0 = start of range, 100 = end. + +## Named Scroll 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. + +--- + +## Rule 1: ViewProgress with keyframeEffect or namedEffect + +**Use Case**: Scroll-driven CSS-based effects + +**Pattern**: ```typescript { key: '[SOURCE_KEY]', trigger: 'viewProgress', - conditions: ['[CONDITION_NAME]'], // optional: e.g. 'prefers-motion', 'desktop-only' + conditions: ['[CONDITION_NAME]'], // optional 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] }, + // Use exactly one of namedEffect or keyframeEffect: + namedEffect: { type: '[NAMED_EFFECT]' }, // OR + keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [EFFECT_KEYFRAMES] }, rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } }, rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } }, easing: '[EASING_FUNCTION]', @@ -28,351 +39,95 @@ } ``` -## 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 - -| 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 | +**Variables**: -## Range Reference +- `[SOURCE_KEY]`: Unique identifier for element that triggers when scrolled through viewport +- `[TARGET_KEY]`: Unique identifier for element to animate (can be same as source or different) +- `[NAMED_EFFECT]`: Preset name from Named Scroll Effects list above +- `[EFFECT_NAME]`: Unique name for custom keyframe effect +- `[EFFECT_KEYFRAMES]`: Array of keyframe objects defining CSS property transitions +- `[RANGE_NAME]`: Scroll range name — `'cover'` for full visibility span, `'entry'`/`'exit'` for partial phases, `'contain'` while contained in viewport - typically while in a stuck `position: stikcy` 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 +- `[FILL_MODE]`: Almost always `'both'` — ensures the effect holds before entering and after exiting its active range +- `[UNIQUE_EFFECT_ID]`: Optional identifier for referencing the effect externally -| 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 | +--- -## Named Scroll 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. +## Rule 2: ViewProgress with customEffect -## Examples +**Use Case**: Scroll-driven effects requiring JavaScript logic (e.g., changing SVG attributes, controlling WebGL/WebGPU effects) -### Example 1: Named Effect (Parallax) +**Pattern**: ```typescript { - key: 'hero-section', - 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) - -```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' - } - ] -} -``` - -### Example 3: Custom Effect (Dynamic Content) - -```typescript -{ - key: 'text-section', + key: '[SOURCE_KEY]', trigger: 'viewProgress', + conditions: ['[CONDITION_NAME]'], // optional effects: [ { - key: 'animated-text', + key: '[TARGET_KEY]', 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 ? '|' : ''); - - 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' - } - ] -} -``` - -### Example 4: Multi-Range (Entry + Exit on the same element) - -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. - -```typescript -{ - key: 'feature-card', - trigger: 'viewProgress', - 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)' } - ] - }, - rangeStart: { name: 'entry', offset: { unit: 'percentage', value: 0 } }, - rangeEnd: { name: 'entry', offset: { unit: 'percentage', value: 60 } }, - easing: 'ease-out', - fill: 'both' - }, - // Animate OUT as element exits viewport - { - key: 'feature-card', - keyframeEffect: { - name: 'card-out', - keyframes: [ - { opacity: '1', transform: 'translateY(0)' }, - { opacity: '0', transform: 'translateY(-40px)' } - ] + // progress is 0–1 within the specified range + [CUSTOM_LOGIC] }, - rangeStart: { name: 'exit', offset: { unit: 'percentage', value: 40 } }, - rangeEnd: { name: 'exit', offset: { unit: 'percentage', value: 100 } }, - easing: 'ease-in', - fill: 'both' + rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } }, + rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } }, + fill: '[FILL_MODE]', + effectId: '[UNIQUE_EFFECT_ID]' } ] } ``` -## Advanced Patterns +**Variables**: -### Multi-Range ViewProgress Effects +- `[SOURCE_KEY]` / `[TARGET_KEY]`: Same as Rule 1 +- `[CUSTOM_LOGIC]`: JavaScript that uses `progress` (0–1) to apply the effect. Avoid layout/style reads inside the callback for smooth performance. +- `[RANGE_NAME]` / `[START_PERCENTAGE]` / `[END_PERCENTAGE]`: Same as Rule 1 +- `[FILL_MODE]`: Almost always `'both'` +- `[UNIQUE_EFFECT_ID]`: Optional identifier for referencing the effect externally -Combining different ranges for complex scroll animations: +--- -```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' - } - ] -} -``` +## Rule 3: ViewProgress with Tall Wrapper + Sticky Container (contain range) -### ViewProgress with Conditional Behavior +**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 -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. -```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** (`[SOURCE_KEY]`): An element with enough height to create scroll distance (e.g., `height: 300vh`). This is the ViewTimeline source. +- **Sticky container** (`[TARGET_KEY]` or parent of targets): A direct child with `position: sticky; top: 0; height: 100vh` that stays fixed in the viewport while the wrapper scrolls past. +- **Animated elements**: Children of the sticky container that receive the effects. -Orchestrating multiple elements with viewProgress: +**Pattern**: ```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: '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' + 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: '[FILL_MODE]', + effectId: '[UNIQUE_EFFECT_ID]' } ] } ``` -## 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]` / `[FILL_MODE]` / `[UNIQUE_EFFECT_ID]`: Same as Rule 1 From 05bb64acff37ecafefdbd7d9d03f3f066e13ca17 Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Sun, 15 Mar 2026 22:51:18 +0200 Subject: [PATCH 07/13] Small cleanup --- packages/interact/rules/viewprogress.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/interact/rules/viewprogress.md b/packages/interact/rules/viewprogress.md index 50deb9f..ad4c49c 100644 --- a/packages/interact/rules/viewprogress.md +++ b/packages/interact/rules/viewprogress.md @@ -26,9 +26,11 @@ From `@wix/motion-presets` scroll animations: ParallaxScroll, MoveScroll, FadeSc effects: [ { key: '[TARGET_KEY]', - // Use exactly one of namedEffect or keyframeEffect: - namedEffect: { type: '[NAMED_EFFECT]' }, // OR + // --- pick ONE of the two effect types --- + namedEffect: { type: '[NAMED_EFFECT]' }, + // OR keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [EFFECT_KEYFRAMES] }, + rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } }, rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } }, easing: '[EASING_FUNCTION]', @@ -68,7 +70,7 @@ From `@wix/motion-presets` scroll animations: ParallaxScroll, MoveScroll, FadeSc effects: [ { key: '[TARGET_KEY]', - customEffect: (element, progress) => { + customEffect: (element: Element, progress: number) => { // progress is 0–1 within the specified range [CUSTOM_LOGIC] }, From 25e2a51a4b2da0e5ee1b2c82639e1742b795c67b Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Mon, 16 Mar 2026 11:26:31 +0200 Subject: [PATCH 08/13] Revisit viewenter.md; Added customEffect rules to hover and click; Fixes to viewprogress.md --- packages/interact/rules/click.md | 38 +- packages/interact/rules/hover.md | 38 +- packages/interact/rules/viewenter.md | 959 +++--------------------- packages/interact/rules/viewprogress.md | 8 +- 4 files changed, 173 insertions(+), 870 deletions(-) diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md index a026fed..43efe35 100644 --- a/packages/interact/rules/click.md +++ b/packages/interact/rules/click.md @@ -29,7 +29,7 @@ Always include `fill: 'both'` so the effect remains applied while finished and i keyframes: [KEYFRAMES], }, // OR - namedEffect: { type: '[NAMED_EFFECT_TYPE]' }, + namedEffect: [NAMED_EFFECT_DEFINITION], fill: '[FILL_MODE]', reversed: [INITIAL_REVERSED_BOOL], @@ -55,7 +55,7 @@ Always include `fill: 'both'` so the effect remains applied while finished and i - `'state'` — pauses/resumes the animation on each click. Useful for continuous loops (`iterations: Infinity`). - `[KEYFRAMES]` — WAAPI-style keyframes format as array of keyframe objects or object of properties to arrays of values. - `[EFFECT_NAME]` — arbitrary string identifier for a `keyframeEffect`. -- `[NAMED_EFFECT_TYPE]` — pre-built effect from `@wix/motion-presets` (e.g. `'FadeIn'`, `'SlideIn'`, `'Pulse'`, `'Breathe'`). +- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. - `[INITIAL_REVERSED_BOOL]` — optional. `true` to start in the "played" state so the first click reverses the animation. - `[FILL_MODE]` — usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished. - `[DURATION_MS]` — animation duration in milliseconds. Typical click range: 100–500. @@ -124,7 +124,39 @@ Use `transition` when all properties share timing. Use `transitionProperties` wh --- -## Rule 3: Sequences +## Rule 3: customEffect with PointerTriggerParams + +Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the element and a `progress` value (0–1) driven by the animation timeline. + +```typescript +{ + key: '[SOURCE_KEY]', + trigger: 'click', + params: { + type: '[POINTER_TYPE]' + }, + effects: [ + { + key: '[TARGET_KEY]', + customEffect: [CUSTOM_EFFECT_CALLBACK], + duration: [DURATION_MS], + easing: '[EASING_FUNCTION]' + } + ] +} +``` + +### Variables + +- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. +- `[POINTER_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 4: Sequences Use sequences when a click should sync/stagger animations across multiple elements. diff --git a/packages/interact/rules/hover.md b/packages/interact/rules/hover.md index 30b1d2c..90fd361 100644 --- a/packages/interact/rules/hover.md +++ b/packages/interact/rules/hover.md @@ -36,7 +36,7 @@ Always include `fill: 'forwards'` or `fill: 'both'` so the effect remains applie keyframes: [KEYFRAMES], }, // OR - namedEffect: { type: '[NAMED_EFFECT_TYPE]' }, + namedEffect: [NAMED_EFFECT_DEFINITINO], fill: '[FILL_MODE]', duration: [DURATION_MS], @@ -60,7 +60,7 @@ Always include `fill: 'forwards'` or `fill: 'both'` so the effect remains applie - `'state'` — pauses/resumes the animation on enter/leave. Useful for continuous loops (`iterations: Infinity`). - `[KEYFRAMES]` - WAAPI-style keyframes format as array of keyframe objects or object of properties to arrays of values. - `[EFFECT_NAME]` — arbitrary string identifier for a `keyframeEffect`. -- `[NAMED_EFFECT_TYPE]` — pre-built effect from `@wix/motion-presets` (e.g. `'FadeIn'`, `'SlideIn'`, `'Pulse'`, `'Breathe'`). +- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. - `[FILL_MODE]` — usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished. - `[DURATION_MS]` — animation duration in milliseconds. Typical hover range: 150–400. - `[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`. @@ -127,7 +127,39 @@ Use `transition` when all properties share timing. Use `transitionProperties` wh --- -## Rule 3: Sequences +## Rule 3: customEffect with PointerTriggerParams + +Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the element and a `progress` value (0–1) driven by the animation timeline. + +```typescript +{ + key: '[SOURCE_KEY]', + trigger: 'hover', + params: { + type: '[POINTER_TYPE]' + }, + effects: [ + { + key: '[TARGET_KEY]', + customEffect: [CUSTOM_EFFECT_CALLBACK], + duration: [DURATION_MS], + easing: '[EASING_FUNCTION]' + } + ] +} +``` + +### Variables + +- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. +- `[POINTER_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 4: Sequences Use sequences when a hover should sync/stagger animations across multiple elements. diff --git a/packages/interact/rules/viewenter.md b/packages/interact/rules/viewenter.md index 5bd6291..1d94472 100644 --- a/packages/interact/rules/viewenter.md +++ b/packages/interact/rules/viewenter.md @@ -1,554 +1,163 @@ # 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 +> **Important:** 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. -**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]' - } - ] -} -``` +## Preventing Flash of Unstyled Content (FOUC) -**Variables**: +Use `generate(config)` from `@wix/interact` server-side or at build time to produce critical CSS that hides entrance elements until their animation plays. -- `[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 +**Usage:** -**Example - Hero Section Entrance**: +- If possible SHOULD be called server-side or at build time - possible also in client, e.g. when entire content is initially hidden +- MUST set `data-interact-initial="true"` on the root element, i.e. with `data-interact-key`, or `` for React integration +- 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 +- For `repeat`/`alternate`/`state` types, MUST manually apply the initial keyframe as style on the target element and use `fill: 'backwards'` or `fill: 'both'` to keep the effect applied when finished, rewound, or reversed ```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' - } - ] -} -``` - -**Example - Content Block Fade In**: +import { generate } from '@wix/interact'; -```typescript -{ - key: 'content-block', - trigger: 'viewEnter', - params: { - type: 'once', - threshold: 0.5 +const config: InteractConfig = { + interactions: [ + { + key: 'hero', + trigger: 'viewEnter', + params: { type: 'once', threshold: 0.2 }, + effects: [{ namedEffect: { type: 'FadeIn' }, duration: 800 }], }, - effects: [ - { - key: 'content-block', - namedEffect: { - type: 'FadeIn' - }, - duration: 800, - easing: 'ease-out', - fill: 'backwards' - } - ] -} -``` - ---- - -## Rule 2: ViewEnter with Repeat Type and Separate Source/Target + ], +}; -**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) +const css = generate(config); -**When to Apply**: +const html = ` + + + + + +
...
+
+ +`; +``` -- 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 +## Rule 1: keyframeEffect / namedEffect with ViewEnterParams -**Pattern**: +Use `keyframeEffect` or `namedEffect` when the viewEnter should play an animation (CSS or WAAPI). Pair with `params: ViewEnterParams` to configure the IntersectionObserver trigger. ```typescript { - key: '[OBSERVER_KEY]', + key: '[SOURCE_KEY]', trigger: 'viewEnter', params: { - type: 'repeat', + type: '[VIEW_ENTER_TYPE]', 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]' - } - ] -} -``` - -**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**: + key: '[TARGET_KEY]', -```typescript -{ - key: 'image-trigger-zone', - trigger: 'viewEnter', - params: { - type: 'repeat', - threshold: 0.1, - inset: '-50px' - }, - effects: [ - { - key: 'background-image', + // --- pick ONE of the two effect types --- keyframeEffect: { - name: 'image-reveal', - keyframes: [ - { filter: 'blur(20px) brightness(0.7)', transform: 'scale(1.1)' }, - { filter: 'blur(0) brightness(1)', transform: 'scale(1)' } - ] + name: '[EFFECT_NAME]', + keyframes: [KEYFRAMES], }, - duration: 600, - easing: 'ease-out', - fill: 'backwards' - } - ] -} -``` - -**Example - Counter Animation Repeat**: - -```typescript -{ - key: 'stats-section', - trigger: 'viewEnter', - params: { - type: 'repeat', - threshold: 0.6 - }, - 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 - -**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) - -**When to Apply**: + // OR + namedEffect: [NAMED_EFFECT_DEFINITION], -- 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 - -**Pattern**: - -```typescript -{ - key: '[OBSERVER_KEY]', - trigger: 'viewEnter', - params: { - type: 'alternate', - threshold: [VISIBILITY_THRESHOLD], - inset: '[VIEWPORT_INSETS]' - }, - effects: [ - { - key: '[ANIMATION_TARGET_KEY]', - [EFFECT_TYPE]: [EFFECT_DEFINITION], + fill: '[FILL_MODE]', duration: [DURATION_MS], easing: '[EASING_FUNCTION]', + delay: [DELAY_MS], + iterations: [ITERATIONS], + alternate: [ALTERNATE_BOOL], effectId: '[UNIQUE_EFFECT_ID]' } ] } ``` -**Variables**: -Same as Rule 2 +### Variables -**Example - Content Reveal with Hide**: - -```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' - } - ] -} -``` - -**Example - Navigation Bar Reveal**: - -```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' - } - ] -} -``` +- `[SOURCE_KEY]` — identifier matching the `data-interact-key` attribute on the element that is observed for viewport intersection. +- `[TARGET_KEY]` — identifier matching the `data-interact-key` attribute on the element that animates. +- `[VIEW_ENTER_TYPE]` — `ViewEnterParams.type`. One of: + - `'once'` — plays once when the element first enters the viewport and never again. Source and target may be the same element. + - `'repeat'` — restarts the animation every time the element enters the viewport. Use separate source and target. + - `'alternate'` — plays forward on enter, reverses on leave. Use separate source and target. + - `'state'` — pauses/resumes the animation on enter/leave. Useful for continuous loops (`iterations: Infinity`). Use separate source and target. +- `[VISIBILITY_THRESHOLD]` — number between 0–1 indicating how much of the source element must be visible to trigger (e.g. `0.3` = 30%). +- `[VIEWPORT_INSETS]` — string adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it). +- `[KEYFRAMES]` — WAAPI-style keyframes format as array of keyframe objects or object of properties to arrays of values. +- `[EFFECT_NAME]` — arbitrary string identifier for a `keyframeEffect`. +- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. +- `[FILL_MODE]` — `'backwards'` for entrance animations (applies initial keyframe before playing), `'both'` for looping or state-based animations. +- `[DURATION_MS]` — animation duration in milliseconds. Typical entrance range: 500–1200. +- `[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 (pair with `type: 'state'`). +- `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration. +- `[UNIQUE_EFFECT_ID]` — optional. String identifier used for animation chaining or sequence references. --- -## Rule 4: ViewEnter with State Type for Loop Animations - -**Use Case**: Looping animations that start when element enters viewport and can be paused/resumed (e.g., ambient animations, loading states, decorative effects) +## Rule 2: customEffect with ViewEnterParams -**When to Apply**: - -- 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 - -**Pattern**: +Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the element and a `progress` value (0–1) driven by the animation timeline. ```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], + customEffect: [CUSTOM_EFFECT_CALLBACK], 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**: - -```typescript -{ - key: 'floating-elements', - trigger: 'viewEnter', - params: { - type: 'state', - threshold: 0.4 - }, - effects: [ - { - key: 'floating-icon', - keyframeEffect: { - name: 'floating-loop', - keyframes: [ - { transform: 'translateY(0)' }, - { transform: 'translateY(-20px)' }, - { transform: 'translateY(0)' } - ] - }, - duration: 3000, - easing: 'ease-in-out', - iterations: Infinity, - alternate: false, - effectId: 'floating-loop' - } - ] -} -``` - -**Example - Breathing Light Effect**: +### Variables -```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' - } - ] -} -``` +- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. +- `[VIEW_ENTER_TYPE]` — same as Rule 1. +- `[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 5: Threshold and Viewport Intersection Parameters - -**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 +## Rule 3: Sequences with ViewEnterParams -**Pattern**: +Use sequences when a viewEnter should sync/stagger animations across multiple elements. ```typescript { key: '[SOURCE_KEY]', trigger: 'viewEnter', params: { - type: '[BEHAVIOR_TYPE]', - threshold: [PRECISE_THRESHOLD], - inset: '[VIEWPORT_ADJUSTMENT]' - }, - effects: [ - { - key: '[TARGET_KEY]', - [EFFECT_TYPE]: [EFFECT_DEFINITION], - 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)' - } - ] -} -``` - -**Example - Late Trigger for Precise Timing**: - -```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 - } - ] - } - ] -} -``` - ---- - -## 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**: - -- 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`)**: - -```typescript -{ - key: '[CONTAINER_KEY]', - trigger: 'viewEnter', - params: { - type: '[BEHAVIOR_TYPE]', - threshold: [VISIBILITY_THRESHOLD] + type: '[VIEW_ENTER_TYPE]', + threshold: [VISIBILITY_THRESHOLD], + inset: '[VIEWPORT_INSETS]' }, sequences: [ { @@ -565,395 +174,29 @@ 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: +The referenced `effectId` 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: { - 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]` — same as Rule 1. +- `[VIEW_ENTER_TYPE]` — same as Rule 1. +- `[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. +- `[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 ad4c49c..e9b1276 100644 --- a/packages/interact/rules/viewprogress.md +++ b/packages/interact/rules/viewprogress.md @@ -6,10 +6,6 @@ These rules help generate scroll-driven interactions using the `@wix/interact` l **Offset semantics:** Values can be as a `string` representing CSS value, or `number` representing percentages. Positive offset values move the effective range forward along the scroll axis. 0 = start of range, 100 = end. -## Named Scroll 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. - --- ## Rule 1: ViewProgress with keyframeEffect or namedEffect @@ -27,7 +23,7 @@ From `@wix/motion-presets` scroll animations: ParallaxScroll, MoveScroll, FadeSc { key: '[TARGET_KEY]', // --- pick ONE of the two effect types --- - namedEffect: { type: '[NAMED_EFFECT]' }, + namedEffect: [NAMED_EFFECT_DEFINITION], // OR keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [EFFECT_KEYFRAMES] }, @@ -45,7 +41,7 @@ From `@wix/motion-presets` scroll animations: ParallaxScroll, MoveScroll, FadeSc - `[SOURCE_KEY]`: Unique identifier for element that triggers when scrolled through viewport - `[TARGET_KEY]`: Unique identifier for element to animate (can be same as source or different) -- `[NAMED_EFFECT]`: Preset name from Named Scroll Effects list above +- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. - `[EFFECT_NAME]`: Unique name for custom keyframe effect - `[EFFECT_KEYFRAMES]`: Array of keyframe objects defining CSS property transitions - `[RANGE_NAME]`: Scroll range name — `'cover'` for full visibility span, `'entry'`/`'exit'` for partial phases, `'contain'` while contained in viewport - typically while in a stuck `position: stikcy` container From 5f6c703640547d886bb881e8bcf9ddf31c2205a9 Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Mon, 16 Mar 2026 11:30:20 +0200 Subject: [PATCH 09/13] Fixed format --- packages/interact/rules/viewprogress.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/interact/rules/viewprogress.md b/packages/interact/rules/viewprogress.md index e9b1276..5f8600c 100644 --- a/packages/interact/rules/viewprogress.md +++ b/packages/interact/rules/viewprogress.md @@ -93,7 +93,6 @@ These rules help generate scroll-driven interactions using the `@wix/interact` l **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 - **Layout Structure**: - **Tall wrapper** (`[SOURCE_KEY]`): An element with enough height to create scroll distance (e.g., `height: 300vh`). This is the ViewTimeline source. From 1ac72ae393347393f4c71d62f0554a6326ff5746 Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Mon, 16 Mar 2026 23:12:46 +0200 Subject: [PATCH 10/13] Revisit pointermove rules; Added missing composite effect property in types --- packages/interact/rules/pointermove.md | 1568 +++--------------------- packages/interact/src/types.ts | 2 + 2 files changed, 167 insertions(+), 1403 deletions(-) diff --git a/packages/interact/rules/pointermove.md b/packages/interact/rules/pointermove.md index 608ea1c..61ab57c 100644 --- a/packages/interact/rules/pointermove.md +++ b/packages/interact/rules/pointermove.md @@ -1,969 +1,89 @@ # 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 the `@wix/interact` library. PointerMove triggers create real-time animations that respond to pointer movement over elements or the entire viewport. -## Core Concepts +## Trigger Source Elements with `hitArea: 'self'` -### Effect Types for PointerMove +When using `hitArea: 'self'`, the source element is the hit area for pointer tracking: -The `pointerMove` trigger provides 2D progress (x and y coordinates). You can use: +- The source element **MUST NOT** have `pointer-events: none` — it needs to receive pointer events to track mouse movement. +- **MUST AVOID** using the same element as both source and target with `transform` effects. The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. -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'`) - -### Hit Area Configuration (`hitArea`) - -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 - } - ] -} -``` - -**Example - Magnetic Effect with Distance Calculation**: - -```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; +## PointerMoveParams - element.style.transform = `translate(${moveX}px, ${moveY}px)`; - }, - centeredToTarget: true - } - ] -} +`params` object for `pointerMove` interactions: + +```typescript +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 hover effects. + - `'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: ```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: [ /* ... */ ] } ] } @@ -971,104 +91,48 @@ For smoother animations, you can use `transitionDuration` and `transitionEasing` --- -## Rule 9: Multi-Element Custom Parallax with customEffect +## Rule 1: namedEffect -**Use Case**: Complex parallax effects with custom physics or non-standard transformations across multiple layers. - -**When to Apply**: - -- For parallax with custom easing or physics -- When layers need different calculation methods -- For effects combining multiple CSS properties - -**Pattern**: +Use pre-built mouse presets from `@wix/motion-presets` that handle 2D progress internally. ```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] - }, - centeredToTarget: true - }, - { - key: '[LAYER_2_KEY]', - customEffect: (element, progress) => { - [LAYER_2_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]' } ] } ``` -**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 `data-interact-key` attribute on the element that tracks mouse movement. +- `[TARGET_KEY]` — identifier matching the `data-interact-key` attribute 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 (e.g., `'Tilt3DMouse'`, `'Track3DMouse'`, `'SwivelMouse'`, `'TrackMouse'`, `'AiryMouse'`, `'BounceMouse'`, `'ScaleMouse'`, `'BlurMouse'`, `'SpinMouse'`, `'SkewMouse'`). +- `[EFFECT_PROPERTIES]` — preset-specific properties (e.g., `angle`, `perspective`, `distance`, `axis`, `scale`). +- `[CENTERED_TO_TARGET]` — `true` or `false`. See **Centering with `centeredToTarget`** above. +- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing transitions between progress updates. +- `[TRANSITION_EASING]` — optional. Easing for the smoothing transition (e.g., `'easeOut'`). --- -## 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**: - -- 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 +## Rule 2: keyframeEffect with Single 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 +140,155 @@ 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] + centeredToTarget: [CENTERED_TO_TARGET], + transitionDuration: [TRANSITION_DURATION_MS], + transitionEasing: '[TRANSITION_EASING]', + effectId: '[UNIQUE_EFFECT_ID]' } ] } ``` -**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', - }, - }, -} -``` - -**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]` — arbitrary string name for the keyframe effect. +- `[START_KEYFRAME]` — CSS keyframe at progress 0 (left/top edge). +- `[CENTER_KEYFRAME]` - optoinal. 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 transitions 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. - -**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**: +## Rule 3: Two keyframeEffects with Two Axes and `composite` -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' }, + params: { hitArea: '[HIT_AREA]', axis: 'x' }, effects: [ { - key: 'composite-add-ball', - effectId: 'scale-x-effect', - }, - ], + key: '[TARGET_KEY]', + effectId: '[X_EFFECT_ID]' + } + ] }, { - key: 'composite-add-container', + key: '[SOURCE_KEY]', trigger: 'pointerMove', - params: { hitArea: 'self', axis: 'y' }, + params: { hitArea: '[HIT_AREA]', axis: 'y' }, effects: [ { - key: 'composite-add-ball', - effectId: 'scale-y-effect', - }, - ], - }, + 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]` — distinct string identifiers for the X-axis and Y-axis effects, referenced from the top-level `effects` map. +- `[X_EFFECT_NAME]` / `[Y_EFFECT_NAME]` — arbitrary 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. +- `[FILL_MODE]` — typically `'both'` to ensure the effect applies before entering and after exiting the effect's active range. +- `[COMPOSITE_OPERATION]` — `'add'` or `'accumulate'`. Required when both effects animate the same property so their values combine rather than override each other. +- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing transitions 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' - }, - centeredToTarget: true - }, - { - key: 'vertical-indicator', - namedEffect: { - type: 'ScaleMouse', - scale: 1.2, - distance: { value: 150, unit: 'px' }, - axis: 'vertical' + key: '[TARGET_KEY]', + customEffect: (element: Element, progress: Progress) => { + [CUSTOM_ANIMATION_LOGIC] }, - centeredToTarget: true + centeredToTarget: [CENTERED_TO_TARGET], + transitionDuration: [TRANSITION_DURATION_MS], + transitionEasing: '[TRANSITION_EASING]' } ] } ``` ---- - -## 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 transitions between progress updates. +- `[TRANSITION_EASING]` — optional. Easing for the smoothing transition (e.g., `'easeOut'`). diff --git a/packages/interact/src/types.ts b/packages/interact/src/types.ts index d04e885..03ea970 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; From d9641cc314362ec49d8294670850ad7ca6e7b34f Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Tue, 17 Mar 2026 21:53:01 +0200 Subject: [PATCH 11/13] Revisit integration.md; Added TOCs --- packages/interact/rules/click.md | 7 + packages/interact/rules/hover.md | 7 + packages/interact/rules/integration.md | 412 +++++++++--------------- packages/interact/rules/pointermove.md | 12 + packages/interact/rules/viewenter.md | 7 + packages/interact/rules/viewprogress.md | 6 + 6 files changed, 187 insertions(+), 264 deletions(-) diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md index 43efe35..b2314a7 100644 --- a/packages/interact/rules/click.md +++ b/packages/interact/rules/click.md @@ -4,6 +4,13 @@ This document contains rules for generating click-triggered interactions in `@wi **Accessible click**: Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space). +## Table of Contents + +- [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 1: keyframeEffect / namedEffect with PointerTriggerParams diff --git a/packages/interact/rules/hover.md b/packages/interact/rules/hover.md index 90fd361..4622add 100644 --- a/packages/interact/rules/hover.md +++ b/packages/interact/rules/hover.md @@ -11,6 +11,13 @@ To avoid this, use a **separate source and target**: - `key` (source) — a stable wrapper element that receives the pointer events. - Effect `key` or `selector` (target) — the other/inner element that actually animates. +## Table of Contents + +- [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 1: keyframeEffect / namedEffect with PointerTriggerParams diff --git a/packages/interact/rules/integration.md b/packages/interact/rules/integration.md index 7dd8c2b..b4d54c3 100644 --- a/packages/interact/rules/integration.md +++ b/packages/interact/rules/integration.md @@ -1,233 +1,196 @@ # @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) +- [Configuration Schema](#configuration-schema) + - [InteractConfig](#interactconfig) + - [Interaction](#interaction) + - [Element Selection](#element-selection) +- [Triggers](#triggers) +- [Sequences](#sequences) +- [Named Effects & registerEffects](#named-effects--registereffects) +- [Critical CSS (FOUC Prevention)](#critical-css-fouc-prevention) +- [Static API](#static-api) -## 2. Integrations +--- -### `web` :: using Custom Elements +## Entry Points -#### 1. Basic Setup - -**Usage:** +### 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. - -**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 value unique 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: { - // ... - }, -}; - -// Initialize the interact instance -const interact = Interact.create(config); +Interact.create(config); ``` -#### 2. HTML Setup +Replace target elements with ``: + +```tsx +import { Interaction } from '@wix/interact/react'; + + + ... + +``` **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. +- MUST set `tagName` to the HTML tag of the element being replaced. +- MUST set `interactKey` to a unique string within the page. -**Usage:** +Alternatively, use `createInteractRef` to attach interactions to an existing element: ```tsx -import { Interaction } from '@wix/interact/react'; +import { createInteractRef } from '@wix/interact/react'; -function MyComponent() { - return ( - - Hello, animated world! - - ); -} +const ref = createInteractRef('hero'); +
...
``` -## 3. Configuration Schema - -The `InteractConfig` object defines the behavior. +### Vanilla JS ```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) -}; +import { Interact } from '@wix/interact'; + +const interact = Interact.create(config); +interact.add(element, 'hero'); ``` -### Interaction Definition +**Rules:** -```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) -} -``` +- Call `add(element, key)` after elements exist in the DOM. +- Call `remove(key)` to unregister all interactions for a key. -### Element Selection Hierarchy +--- -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`. +## Configuration Schema -## 4. Generating Critical CSS for Entrance Animations +### InteractConfig -### `generate(config)` +```typescript +type InteractConfig = { + interactions: Interaction[]; + effects: Record; + sequences?: Record; + conditions?: Record; +}; +``` -Generates critical CSS styles that prevent flash-of-unstyled-content (FOUC) for elements with `viewEnter` entrance animations. +| Field | Description | +|:------|:------------| +| `interactions` | Required. Array of interaction definitions binding triggers to effects. | +| `effects` | Required. Reusable named effects, referenced by `effectId`. | +| `sequences` | Optional. Reusable sequence definitions, referenced by `sequenceId`. | +| `conditions` | Optional. Named conditions (media/container queries), referenced by ID. | -**Rules:** +Each call `Interact.create(config)` creates a new `Interact` instance. -- 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. +### Interaction -**Usage:** +```typescript +{ + key: 'hero', // Matches data-interact-key / interactKey + trigger: 'viewEnter', // Trigger type + params?: { type: 'once' }, // Trigger-specific parameters + selector?: '.child', // CSS selector to refine target within the element + listContainer?: '.grid', // CSS selector for a list container + listItemSelector?: '.item', // CSS selector for items within listContainer + conditions?: ['Desktop'], // Array of condition IDs + effects?: [ ... ], // Effects to apply + sequences?: [ ... ], // Sequences to apply +} +``` -```javascript -import { generate } from '@wix/interact/web'; +### Element Selection -const config = { - /*...*/ -}; +Resolved in order of priority: -// Generate CSS at build time or on server -const css = generate(config); +1. **`listContainer` + `listItemSelector`** — matches items within the container. +2. **`listContainer` only** — targets immediate children of the container. +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). -// Include in your HTML template -const html = ` - - - - - - - -
- ... -
-
- - - -`; -``` +--- + +## Triggers -## 5. Triggers & Behaviors +| Trigger | Description | Key Parameters | 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`, `threshold` (0–1), `inset` | [viewenter.md](./viewenter.md) | +| `viewProgress` | Scroll-driven (ViewTimeline) | Uses effect `rangeStart`/`rangeEnd` | [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 | — | -| 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 | -- | +Use `type` (via `PointerTriggerParams`) for keyframe/named effects, `method` (via `StateParams`) for transition effects. -## 5b. Sequences (Coordinated Stagger) +--- -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). +## Sequences -### 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 + offset: 100, // ms between consecutive items + offsetEasing: 'quadIn', // stagger distribution curve + delay: 0, // base delay before the sequence starts + effects: [ { effectId: 'card-entrance', listContainer: '.card-grid' }, ], } ``` -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 them by `sequenceId`: ```typescript { sequences: { - 'stagger-entrance': { offset: 80, offsetEasing: 'quadIn', effects: [{ effectId: 'fade-up', listContainer: '.items' }] }, + 'stagger-fade': { offset: 80, offsetEasing: 'quadIn', effects: [{ effectId: 'fade-up', listContainer: '.items' }] }, }, interactions: [ - { key: 'section', trigger: 'viewEnter', params: { type: 'once' }, sequences: [{ sequenceId: 'stagger-entrance' }] }, + { key: 'section', trigger: 'viewEnter', params: { type: 'once' }, 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`. +## Named Effects & registerEffects -**Install:** - -```bash -> npm install @wix/motion-presets -``` - -**Import and register:** +Register `@wix/motion-presets` before calling `Interact.create`: ```typescript import { Interact } from '@wix/interact/web'; @@ -236,132 +199,53 @@ import * as presets from '@wix/motion-presets'; Interact.registerEffects(presets); ``` -**Or register only required presets:** +Or register selectively: ```typescript -import { Interact } from '@wix/interact/web'; import { FadeIn, ParallaxScroll } from '@wix/motion-presets'; - Interact.registerEffects({ FadeIn, ParallaxScroll }); ``` +Reference in effects: + ```typescript -{ - namedEffect: { type: 'FadeIn' }, - duration: 800, - easing: 'ease-out' -} +{ namedEffect: { type: 'FadeIn' }, duration: 800, easing: 'ease-out' } ``` -## 7. Examples +For full effect type syntax (`keyframeEffect`, `customEffect`, `TransitionEffect`, `ScrubEffect`), see [full-lean.md](./full-lean.md). -### Basic Hover (Scale) +--- -```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', - }, - ], - }, - ], -}; -``` +## Critical CSS (FOUC Prevention) -### Viewport Entrance +`generate(config)` produces CSS that hides entrance elements until their animation plays. See [viewenter.md](./viewenter.md) for full details. -```typescript -const config = { - interactions: [ - { - key: 'hero', - trigger: 'viewEnter', - params: { type: 'once', threshold: 0.2 }, - effects: [ - { - namedEffect: { type: 'FadeIn' }, - duration: 800, - }, - ], - }, - ], -}; -``` +**Rules:** -### Staggered List Entrance (Sequence) +- Call server-side or at build time. +- Set `data-interact-initial="true"` on the `` (or `initial={true}` on `` in React). +- Only valid for `viewEnter` + `type: 'once'` where source and target are the same element. -```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', - }, - }, -}; -``` +```javascript +import { generate } from '@wix/interact/web'; -### Interactive Toggle (Click) +const css = generate(config); +``` -```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, - }, - ], - }, - ], -}; +Inside ``: +```html + ``` + +--- + +## Static API + +| Method / Property | Description | +|:------------------|:------------| +| `Interact.create(config)` | Initialize with a config. Returns the instance. | +| `Interact.registerEffects(presets)` | Register named effect presets before `create`. | +| `Interact.destroy()` | Tear down all instances. | +| `Interact.forceReducedMotion` | `boolean` — force reduced-motion behavior regardless of OS setting. | +| `Interact.allowA11yTriggers` | `boolean` — enable accessibility triggers. | +| `Interact.setup(options)` | Configure global scroll/pointer/viewEnter options. | diff --git a/packages/interact/rules/pointermove.md b/packages/interact/rules/pointermove.md index 61ab57c..e1bacc6 100644 --- a/packages/interact/rules/pointermove.md +++ b/packages/interact/rules/pointermove.md @@ -2,6 +2,18 @@ These rules help generate pointer-driven interactions using the `@wix/interact` library. PointerMove triggers create real-time animations that respond to pointer movement over elements or the entire viewport. +## Table of Contents + +- [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) + ## Trigger Source Elements with `hitArea: 'self'` When using `hitArea: 'self'`, the source element is the hit area for pointer tracking: diff --git a/packages/interact/rules/viewenter.md b/packages/interact/rules/viewenter.md index 1d94472..ebaa282 100644 --- a/packages/interact/rules/viewenter.md +++ b/packages/interact/rules/viewenter.md @@ -6,6 +6,13 @@ This document contains rules for generating viewport-based interactions using th > **Important:** 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. +## Table of Contents + +- [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) + --- ## Preventing Flash of Unstyled Content (FOUC) diff --git a/packages/interact/rules/viewprogress.md b/packages/interact/rules/viewprogress.md index 5f8600c..d155cee 100644 --- a/packages/interact/rules/viewprogress.md +++ b/packages/interact/rules/viewprogress.md @@ -6,6 +6,12 @@ These rules help generate scroll-driven interactions using the `@wix/interact` l **Offset semantics:** Values can be as a `string` representing CSS value, or `number` representing percentages. Positive offset values move the effective range forward along the scroll axis. 0 = start of range, 100 = end. +## Table of Contents + +- [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) + --- ## Rule 1: ViewProgress with keyframeEffect or namedEffect From f68aa3a4ef29f63ec06d54daa1f6c8cd35ee2287 Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Tue, 17 Mar 2026 21:53:19 +0200 Subject: [PATCH 12/13] Fixed format --- packages/interact/rules/integration.md | 57 ++++++++++++++------------ 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/interact/rules/integration.md b/packages/interact/rules/integration.md index b4d54c3..d5727d6 100644 --- a/packages/interact/rules/integration.md +++ b/packages/interact/rules/integration.md @@ -58,7 +58,7 @@ import { Interaction } from '@wix/interact/react'; ... - +; ``` **Rules:** @@ -72,7 +72,7 @@ Alternatively, use `createInteractRef` to attach interactions to an existing ele import { createInteractRef } from '@wix/interact/react'; const ref = createInteractRef('hero'); -
...
+
...
; ``` ### Vanilla JS @@ -104,12 +104,12 @@ type InteractConfig = { }; ``` -| Field | Description | -|:------|:------------| +| Field | Description | +| :------------- | :---------------------------------------------------------------------- | | `interactions` | Required. Array of interaction definitions binding triggers to effects. | -| `effects` | Required. Reusable named effects, referenced by `effectId`. | -| `sequences` | Optional. Reusable sequence definitions, referenced by `sequenceId`. | -| `conditions` | Optional. Named conditions (media/container queries), referenced by ID. | +| `effects` | Required. Reusable named effects, referenced by `effectId`. | +| `sequences` | Optional. Reusable sequence definitions, referenced by `sequenceId`. | +| `conditions` | Optional. Named conditions (media/container queries), referenced by ID. | Each call `Interact.create(config)` creates a new `Interact` instance. @@ -135,7 +135,7 @@ Resolved in order of priority: 1. **`listContainer` + `listItemSelector`** — matches items within the container. 2. **`listContainer` only** — targets immediate children of the container. -3. **`listContainer` + `selector`** - matches via `querySelector` within each immediate child of the container. +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). @@ -143,16 +143,16 @@ Resolved in order of priority: ## Triggers -| Trigger | Description | Key Parameters | 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`, `threshold` (0–1), `inset` | [viewenter.md](./viewenter.md) | -| `viewProgress` | Scroll-driven (ViewTimeline) | Uses effect `rangeStart`/`rangeEnd` | [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 | — | +| Trigger | Description | Key Parameters | 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`, `threshold` (0–1), `inset` | [viewenter.md](./viewenter.md) | +| `viewProgress` | Scroll-driven (ViewTimeline) | Uses effect `rangeStart`/`rangeEnd` | [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 | — | Use `type` (via `PointerTriggerParams`) for keyframe/named effects, `method` (via `StateParams`) for transition effects. @@ -233,19 +233,22 @@ const css = generate(config); ``` Inside ``: + ```html - + ``` --- ## Static API -| Method / Property | Description | -|:------------------|:------------| -| `Interact.create(config)` | Initialize with a config. Returns the instance. | -| `Interact.registerEffects(presets)` | Register named effect presets before `create`. | -| `Interact.destroy()` | Tear down all instances. | -| `Interact.forceReducedMotion` | `boolean` — force reduced-motion behavior regardless of OS setting. | -| `Interact.allowA11yTriggers` | `boolean` — enable accessibility triggers. | -| `Interact.setup(options)` | Configure global scroll/pointer/viewEnter options. | +| Method / Property | Description | +| :---------------------------------- | :------------------------------------------------------------------ | +| `Interact.create(config)` | Initialize with a config. Returns the instance. | +| `Interact.registerEffects(presets)` | Register named effect presets before `create`. | +| `Interact.destroy()` | Tear down all instances. | +| `Interact.forceReducedMotion` | `boolean` — force reduced-motion behavior regardless of OS setting. | +| `Interact.allowA11yTriggers` | `boolean` — enable accessibility triggers. | +| `Interact.setup(options)` | Configure global scroll/pointer/viewEnter options. | From 5fe9bf979784833dfa51f34c04aafc5d9a6ae83c Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Wed, 18 Mar 2026 14:50:53 +0200 Subject: [PATCH 13/13] Revisit full-lean.md and removed scroll-list.md --- packages/interact/rules/full-lean.md | 883 +++++++++++++------------ packages/interact/rules/scroll-list.md | 748 --------------------- 2 files changed, 449 insertions(+), 1182 deletions(-) delete mode 100644 packages/interact/rules/scroll-list.md diff --git a/packages/interact/rules/full-lean.md b/packages/interact/rules/full-lean.md index 5e1b337..20fd3fe 100644 --- a/packages/interact/rules/full-lean.md +++ b/packages/interact/rules/full-lean.md @@ -1,504 +1,519 @@ -### Basic usage (quick start) - -- 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. - -**For web (Custom Elements):** +# @wix/interact — Rules + +Declarative configuration-driven interaction library. Binds animations to triggers via JSON config. + +## Table of Contents + +- [Quick Start](#quick-start) +- [Element Binding](#element-binding) +- [Config Structure](#config-structure) +- [Interactions](#interactions) + - [Source element resolution](#source-element-resolution-interaction) + - [Target element resolution](#target-element-resolution-effect) +- [Triggers](#triggers) + - [hover / click](#hover--click) + - [viewEnter](#viewenter) + - [viewProgress](#viewprogress) + - [pointerMove](#pointermove) + - [animationEnd](#animationend) +- [Effects](#effects) + - [TimeEffect](#timeeffect-animation-over-time) + - [ScrubEffect](#scrubeffect-scroll--pointer-driven) + - [TransitionEffect](#transitioneffect-css-state-toggle) + - [Animation Payloads](#animation-payloads) +- [Sequences](#sequences) +- [Conditions](#conditions) +- [FOUC Prevention](#fouc-prevention) +- [Common Pitfalls](#common-pitfalls) +- [Static API](#static-api) + +--- + +## Quick Start + +Create the full config up-front and pass it in a single `create` call. Subsequent calls create new `Interact` instances. + +**Web (Custom Elements):** ```ts import { Interact } from '@wix/interact/web'; -import type { InteractConfig } from '@wix/interact'; - -const config: InteractConfig = { - // config-props -}; - Interact.create(config); ``` -**For React:** +**React:** ```ts import { Interact } from '@wix/interact/react'; -import type { InteractConfig } from '@wix/interact'; +Interact.create(config); +``` -const config: InteractConfig = { - // config-props -}; +**Vanilla JS:** -Interact.create(config); +```ts +import { Interact } from '@wix/interact'; +const interact = Interact.create(config); +interact.add(element, 'hero'); // bind after element exists in DOM +interact.remove('hero'); // unregister ``` -### Using `namedEffect` presets (`registerEffects`) +**CDN (no build tools):** + +```html + +``` -Before using `namedEffect`, you must register the presets with the `Interact` instance. Without this, `namedEffect` types will not resolve. +**Registering presets** — required before using `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 -- 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. +- MUST set `tagName` to the replaced element's HTML tag. +- MUST set `interactKey` to a unique string. -**Usage:** +```tsx +import { Interaction } from '@wix/interact/react'; -```javascript -import { generate } from '@wix/interact/web'; + + ... +; +``` + +--- -const config = { - /*...*/ +## Config Structure + +```ts +type InteractConfig = { + interactions: Interaction[]; // REQUIRED + effects?: Record; // reusable effects referenced by effectId + sequences?: Record; // reusable sequences by sequenceId + conditions?: Record; // named guards by id }; +``` -// 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. + +--- + +## Interactions + +Each interaction maps a source element + trigger to one or more effects. + +```ts +{ + key: string; // REQUIRED — matches data-interact-key + trigger: TriggerType; // REQUIRED + params?: TriggerParams; // trigger-specific options + effects?: (Effect | EffectRef)[]; + sequences?: (SequenceConfig | SequenceConfigRef)[]; + conditions?: string[]; // condition ids; all must pass + selector?: string; // CSS selector to refine element selection + listContainer?: string; // CSS selector for list container + listItemSelector?: string; // CSS selector for items within listContainer +} +``` + +At least one of `effects` or `sequences` MUST be provided. + +### Source element resolution (Interaction) + +The source element is the element the trigger attaches to. Resolved in priority order: + +1. **`listContainer` + `listItemSelector`** — trigger attaches to each element matching `listItemSelector` within the `listContainer`. +2. **`listContainer` only** — trigger attaches to each immediate child of the container. +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 the element the effect animates. Resolved in priority order: + +1. **`Effect.key`** — if provided, the target is 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 are animated, following the same priority order as source resolution above. + +--- + +## 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. + +**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 on enter, remove on leave | Toggle per click | +| `'add'` | Add on enter; leave does NOT remove | Add on click | +| `'remove'` | Remove on enter | Remove on click | +| `'clear'` | Clear/reset all states on enter | Clear/reset all states | + +**Hit-area shift warning:** 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. + +### viewEnter + +```ts +params: { + type: 'once' | 'repeat' | 'alternate' | 'state'; + threshold?: number; // 0–1, IntersectionObserver threshold + inset?: string; // vertical rootMargin, e.g. '-100px' +} +``` + +**Critical rule:** When source and target are the **same element**, MUST use `type: 'once'`. For `repeat` / `alternate` / `state`, 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 ViewTimeline. Progress is driven by scroll position. Control the range via `rangeStart`/`rangeEnd` on the `ScrubEffect`. + +**Critical rule:** 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. If using tailwind replace all `overflow-hidden` classes with `oveflow-clip`. + +**Sticky container pattern** — for scroll-driven animations inside a stuck `position: sticky` container: + +- Tall wrapper (`key`): enough height to create scroll distance (e.g., `height: 300vh`). This is the ViewTimeline source. +- Sticky child (`position: sticky; top: 0; height: 100vh`): stays fixed while the wrapper scrolls. +- Use `rangeStart/rangeEnd` with `name: 'contain'` to animate only during the stuck phase. + +### pointerMove + +```ts +params: { + hitArea?: 'self' | 'root'; // 'self' = source element bounds, 'root' = viewport + axis?: 'x' | 'y'; // only for keyframeEffect; selects which axis maps to 0–1 progress +} +``` + +**Rules:** + +- Source element MUST NOT have `pointer-events: none`. +- Avoid using the same element as both source and target with `transform` effects — the transform shifts the hit area. Use `selector` to target a child. +- Use a `(hover: hover)` media condition to disable on touch-only devices. On touch-only devices prefer fallback to `viewEnter` or `viewProgress` based interactions. +- 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` value of `'add'`/`'accumulate'` on the latter effect to combine them. + +**`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 } +``` + +### 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 `effects` registry. See [Target element resolution](#target-element-resolution-effect) for how the target is determined. + +### Common fields -// Include in your HTML template -const html = ` - - - - - - - -
-

Welcome to Our Site

-

This content fades in smoothly without flash

-
-
- - - -`; +```ts +{ + key?: string; // target element key; omit to target the source + effectId?: string; // reference to effects registry (EffectRef) + conditions?: string[]; // all must pass + selector?: string; // CSS selector to refine target + listContainer?: string; + listItemSelector?: string; + composite?: 'replace' | 'add' | 'accumulate'; + fill?: 'none' | 'forwards' | 'backwards' | 'both'; +} ``` -### 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 +**`fill` guidance:** + +- `'both'` — preferred for scroll-driven (`viewProgress`) and pointer-driven (`pointerMove`) effects. Also for `type` of `alternate` or `repeat` `viewEnter`, `hover`, and `click` effects. +- `'backwards'` — good for entrance animations with `type: 'once'` (applies initial keyframe before playing). + +**`composite`** — controls how this effect combines with others on the same property (transforms & filters): + +- `'replace'` (default): fully replaces prior values. +- `'add'`: function values add up sequentially. +- `'accumulate'`: similar function values' arguments add up, new functions add up sequentially. + +### TimeEffect (animation over time) + +Used with `hover`, `click`, `viewEnter`, `animationEnd` triggers. + +```ts { - 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: number; // REQUIRED (ms) + easing?: string; // CSS easing or named easing from `@wix/motion` + delay?: number; // ms + iterations?: number; // >=1 or Infinity, 0 for Infinity + alternate?: boolean; + reversed?: boolean; + fill?: 'none' | 'forwards' | 'backwards' | 'both'; + composite?: 'replace' | 'add' | 'accumulate'; + // + one animation payload (see below) } ``` -### Working with elements +### ScrubEffect (scroll / pointer driven) + +Used with `viewProgress` and `pointerMove` triggers. -#### Web: `` custom element +```ts +{ + rangeStart: RangeOffset; + rangeEnd: RangeOffset; + easing?: string; // CSS easing or named easing from `@wix/motion` + 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 + transitionDelay?: number; + transitionEasing?: string; + // + one animation payload (see below) +} +``` -- 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. +**RangeOffset:** -```html - - - +```ts +{ + name?: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing'; + offset: { value: number; unit: 'percentage' | 'px' | 'em' | 'rem' | 'vh' | 'vw' | 'vmin' | 'vmax' } +} ``` +| 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` | + +### TransitionEffect (CSS state toggle) + +Used with `hover` / `click` triggers. Pair with `StateParams` (`method`). + ```ts -import type { InteractConfig } from '@wix/interact'; - -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) - }, - ], - }, - ], -}; +// 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 } + ] +} ``` -For a different target element: +CSS property names use **camelCase** (e.g. `'backgroundColor'`, `'borderRadius'`). -```html - - - +### Animation Payloads - - Badge - +Exactly one MUST be provided per TimeEffect or ScrubEffect: + +1. **`namedEffect`** (preferred) — pre-built presets from `@wix/motion-presets`. GPU-friendly and tuned. + + ```ts + namedEffect: { type: 'FadeIn', /* preset options */ } + ``` + + Available presets: + + | Category | Presets | + | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | Entrance | `FadeIn`, `SlideIn`, `BounceIn`, `FlipIn`, `ArcIn`, `BlurIn`, `ShuttersIn`, `CurveIn`, `DropIn`, `ExpandIn`, `FloatIn`, `FoldIn`, `GlideIn`, `ShapeIn`, `RevealIn`, `SpinIn`, `TiltIn`, `TurnIn`, `WinkIn` | + | Ongoing | `Pulse`, `Spin`, `Wiggle`, `Bounce`, `Breathe`, `Flash`, `Flip`, `Fold`, `Jello`, `Poke`, `Rubber`, `Swing`, `Cross` | + | Scroll | `ParallaxScroll`, `FadeScroll`, `RevealScroll`, `TiltScroll`, `MoveScroll`, `PanScroll`, `BlurScroll`, `FlipScroll`, `GrowScroll`, `SlideScroll`, `SpinScroll`, `ArcScroll`, `ShapeScroll`, `ShuttersScroll`, `ShrinkScroll`, `SkewPanScroll`, `Spin3dScroll`, `StretchScroll`, `TurnScroll` | + | Mouse | `TrackMouse`, `Tilt3DMouse`, `Track3DMouse`, `SwivelMouse`, `AiryMouse`, `BounceMouse`, `ScaleMouse`, `BlurMouse`, `SpinMouse`, `SkewMouse`, `BlobMouse`, `CustomMouse` | + - Scroll presets (`*Scroll`) with `viewProgress` MUST include `range: 'in' | 'out' | 'continuous'` in options; prefer `'continuous'`. + - Mouse presets are preferred over `keyframeEffect` for `pointerMove` 2D effects. + - Do NOT guess preset option names/types; omit unknown options and rely on defaults. + +2. **`keyframeEffect`** — custom keyframe animations. + + ```ts + keyframeEffect: { name: 'my-effect', keyframes: [{ opacity: 0 }, { opacity: 1 }] } + ``` + + Keyframes use standard CSS/WAAPI object format. Property names are in JS format (cameCase). + +3. **`customEffect`** — imperative update callback. Last resort. + + ```ts + customEffect: (element: Element, progress: number | ProgressObject) => void + ``` + +--- + +## Sequences + +Coordinate multiple effects with staggered timing. Prefer sequences over manual delay stagger. + +```ts +{ + effects: (Effect | EffectRef)[]; // REQUIRED + delay?: number; // ms before sequence starts + offset?: number; // ms offset between consecutive effects + offsetEasing?: string; // offset-stagger distribution curve + sequenceId?: string; // for caching/referencing + conditions?: string[]; +} ``` +`offsetEasing` values: `'linear'`, `'quadIn'`, `'quadOut'`, `'sineOut'`, `'cubicIn'`, `'cubicOut'`, `'cubicInOut'`, `'cubic-bezier(...)'`, or `'linear(...)'`. + +**Example — staggered list entrance:** + ```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) - }, - ], +{ + 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': { + namedEffect: { type: 'FadeIn' }, duration: 600, easing: 'ease-out', fill: 'backwards', }, - ], -}; + }, +} ``` -#### React: `` component +Reusable sequences can be defined in `InteractConfig.sequences` and referenced by `sequenceId`. -- 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. +--- -```tsx -import { Interaction } from '@wix/interact/react'; +## Conditions + +Named guards that gate interactions/effects. -function MyComponent() { - return ( - - Click me - - ); +```ts +conditions: { + 'Desktop': { type: 'media', predicate: '(min-width: 768px)' }, + 'HoverDevice': { type: 'media', predicate: '(hover: hover)' }, + 'ReducedMotion': { type: 'media', predicate: '(prefers-reduced-motion: reduce)' }, + 'OddItems': { type: 'selector', predicate: ':nth-of-type(odd)' }, } ``` -For a different target element: +| Type | Predicate | +| :---------- | :-------------------------------------------------------------- | +| `media` | CSS media query without `@media` (e.g., `'(min-width: 768px)'`) | +| `container` | CSS container query condition | +| `selector` | CSS selector; `&` is replaced with the base element selector | -```tsx -import { Interaction } from '@wix/interact/react'; +Attach via `conditions: ['Desktop']` on interactions, effects, or sequences. All must pass. -function MyComponent() { - return ( - <> - - Click me - - - - Badge - - - ); -} +--- + +## FOUC Prevention + +Use `generate(config)` to create critical CSS that hides elements until their entrance animation plays. + +```ts +import { generate } from '@wix/interact/web'; +const css = generate(config); +// Include in : ``` -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. +**Rules:** + +- Should be called server-side or at build time. Can also be called on client-side and be injected if page content is initially hidden, e.g. behind a loader/splash screen. +- Set `data-interact-initial="true"` on the effect's root element or `` (or `initial={true}` on ``). +- Only valid for `viewEnter` + `type: 'once'` where source and target are the same element. +- Do NOT use for `hover`, `click`, or `viewEnter` with `repeat`/`alternate`/`state`. +- For `repeat`/`alternate`/`state`, manually apply the initial keyframe as a style and use `fill: 'both'`. + +--- + +## Common Pitfalls + +- **`overflow: hidden` breaks `viewProgress`** — use `overflow: clip` instead on all ancestors between source and scroll container. +- **Stacking contexts and `viewProgress`**: Avoid `transform`, `filter`, `perspective`, `opacity < 1`, `will-change`, `contain: paint/layout/size` on the target or its ancestors. These can prevent or freeze ViewTimeline. Apply such styles to an inner child instead. +- **Perspective**: Prefer `transform: perspective(...)` inside keyframes. Use the CSS `perspective` property only when multiple children share the same `perspective-origin`. +- **Unknown preset options**: If you don't know the expected type/structure for a `namedEffect` param, omit it — rely on defaults rather than guessing. +- **Reduced motion**: Use conditions to provide gentler alternatives (shorter durations, fewer transforms, no perpetual motion) for users who prefer reduced motion. + +--- + +## Static API + +| Method / Property | Description | +| :---------------------------------- | :------------------------------------------------------------------ | +| `Interact.create(config)` | Initialize with a config. Returns the instance. | +| `Interact.registerEffects(presets)` | Register named effect presets before `create`. | +| `Interact.destroy()` | Tear down all instances. | +| `Interact.forceReducedMotion` | `boolean` — force reduced-motion behavior. | +| `Interact.allowA11yTriggers` | `boolean` — enable accessibility triggers (`interest`, `activate`). | +| `Interact.setup(options)` | Configure global scroll/pointer/viewEnter options. | diff --git a/packages/interact/rules/scroll-list.md b/packages/interact/rules/scroll-list.md deleted file mode 100644 index 86c7955..0000000 --- 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']`).