# @wix/interact v2.2.2 -- AI Rules Reference
# https://wix.github.io/interact/llms.txt
# 7 files, 2115 lines
--- full-lean.md ---
# @wix/interact — Rules
Declarative configuration-driven interaction library. Binds animations to triggers via JSON config.
## Table of Contents
- [Common Pitfalls](#common-pitfalls)
- [Quick Start](#quick-start)
- [Element Binding](#element-binding)
- [Config Structure](#config-structure)
- [Interactions](#interactions)
- [Triggers](#triggers)
- [hover / click](#hover--click)
- [viewEnter](#viewenter)
- [viewProgress](#viewprogress)
- [pointerMove](#pointermove)
- [animationEnd](#animationend)
- [Effects](#effects)
- [Time-based Effect](#time-based-effect)
- [Scroll / Pointer-driven Effect](#scroll--pointer-driven-effect)
- [State Effect](#stateeffect-css-style-toggle)
- [Animation Payloads](#animation-payloads)
- [Sequences](#sequences)
- [Conditions](#conditions)
- [CSS Generation & FOUC Prevention](#css-generation--fouc-prevention)
- [Element Resolution](#element-resolution)
- [Static API](#static-api)
---
## Common Pitfalls
Each item here is CRITICAL — ignoring any of them will break animations.
- **CRITICAL — `overflow: hidden` breaks `viewProgress`**: Replace with `overflow: clip` on all ancestors between source and scroll container. In Tailwind, replace `overflow-hidden` with `overflow-clip`.
- **CRITICAL**: When using `viewEnter` trigger and 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.
- **CRITICAL - Hit-area shift**: When a hover effect changes the size or position of the hovered element (e.g., `transform: scale(…)`), MUST use a separate source and target elements. Otherwise the hit-area shifts, causing rapid enter/leave.
events and flickering. Use `selector` to target a child element, or set the effect's `key` to a different element.
- **CRITICAL**: For `pointerMove` trigger MUST AVOID using the same element as both source and target with `hitArea: 'self'` and effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation.
- **CRITICAL — Do NOT guess preset options**: If you don't know the expected type/structure for a `namedEffect` param, omit it — rely on defaults rather than guessing.
- **Reduced motion**: Use conditions to provide gentler alternatives (shorter durations, fewer transforms, no perpetual motion) for users who prefer reduced motion. You can also set `Interact.forceReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches` to force a global reduced-motion behavior programmatically.
- **Perspective**: Prefer `transform: perspective(...)` inside keyframes. Use the CSS `perspective` property only when multiple children share the same `perspective-origin`.
---
## Quick Start
```bash
npm install @wix/interact @wix/motion-presets
```
Create the full config up-front and pass it in a single `create` call. Subsequent calls create new `Interact` instances. When creating multiple instances, each manages its own set of interactions independently — use separate instances for isolated component scopes or lazy-loaded sections.
**Web (Custom Elements):**
```ts
import { Interact } from '@wix/interact/web';
const instance = Interact.create(config);
```
The `config` object is an `InteractConfig` containing `interactions` (required), and optionally shared `effects`, `sequences`, and `conditions`.
**React:**
- Wrap the `Interact.create()` call in a `useEffect` hook to prevent it from running on server-side.
- Store the returned instance, and call its `.destroy()` method on the effect's cleanup function.
```ts
import { useEffect } from 'react';
import { Interact } from '@wix/interact/react';
useEffect(() => {
const instance = Interact.create(config);
return () => {
instance.destroy();
};
}, [config]);
```
**Vanilla JS:**
```ts
import { Interact } from '@wix/interact';
const instance = Interact.create(config);
instance.add(element, 'hero'); // bind after element exists in DOM
instance.remove('hero'); // unregister
```
**CDN (no build tools):**
```html
```
**Registering presets** — MUST be called before calling `Interact.create()` with usage of `namedEffect`:
```ts
import * as presets from '@wix/motion-presets';
Interact.registerEffects(presets);
```
Or selectively:
```ts
import { FadeIn, ParallaxScroll } from '@wix/motion-presets';
Interact.registerEffects({ FadeIn, ParallaxScroll });
```
---
## Element Binding
**CRITICAL:** Do NOT add observers/event listeners manually. The runtime binds triggers and effects via element keys.
### 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
```
### React: `` component
- MUST set `tagName` to the replaced element's HTML tag.
- MUST set `interactKey` to a unique string.
```tsx
import { Interaction } from '@wix/interact/react';
...
;
```
---
## Config Structure
```ts
type InteractConfig = {
interactions: Interaction[]; // REQUIRED
effects?: Record; // reusable effects referenced by effectId
sequences?: Record; // reusable sequences by sequenceId
conditions?: Record; // named conditions; keys are condition ids
};
```
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.
**Multiple effects per interaction:** A single interaction can contain multiple effects in its `effects` array. All effects in the same interaction share the same trigger — they all fire together when the trigger activates. Use this to apply different animations to different targets from the same trigger event, rather than creating separate interactions with duplicate trigger configs.
```ts
{
key: string; // REQUIRED — matches data-interact-key / interactKey - the root element
trigger: TriggerType; // REQUIRED
params?: TriggerParams; // trigger-specific options
effects?: (Effect | EffectRef)[]; // possible to add multiple effects for same trigger
sequences?: (SequenceConfig | SequenceConfigRef)[]; // possible to add multiple sequences for same trigger
conditions?: string[]; // ids referencing the top-level conditions map; all must pass
selector?: string; // optional - CSS selector to refine source element selection within the root element
listContainer?: string; // optional — CSS selector for list container
listItemSelector?: string; // optional — CSS selector to filter which children of listContainer are observed as sources
}
```
At least one of `effects` or `sequences` MUST be provided.
For most use cases, `key` alone is sufficient for both source and target resolution. The `selector`, `listContainer`, and `listItemSelector` fields are only needed for advanced patterns (lists, delegated triggers, child targeting). See [Element Resolution](#element-resolution) for details.
---
## Triggers
- **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' | '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: No params needed. Behavior is configured on the effect itself.
- viewEnter: `ViewEnterParams`
- `threshold?`: number in [0,1] describing intersection threshold
- `inset?`: string CSS-style inset for rootMargin/observer geometry
- viewProgress: No trigger params. 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
};
```
### hover / click
For `TimeEffect` (keyframe/named/custom effects), set `triggerType` on the effect. For `StateEffect` (transitions), set `stateAction` on the effect. Do NOT mix `triggerType` and `stateAction` on the same effect.
**`triggerType`** — on `TimeEffect`:
| 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 |
**`stateAction`** — on `StateEffect`:
| Action | hover behavior | click behavior |
| :------------------- | :---------------------------------------------- | :--------------------------- |
| `'toggle'` (default) | Add style state on enter, remove on leave | Toggle style state per click |
| `'add'` | Add style state on enter; leave does NOT remove | Add style state on click |
| `'remove'` | Remove style state on enter | Remove style state on click |
| `'clear'` | Clear/reset all style states on enter | Clear/reset all style states |
### viewEnter
```ts
params: {
threshold?: number; // 0–1, IntersectionObserver threshold
inset?: string; // like view-timeline-inset, e.g. '-100px' or '-50px 0px'
}
// Playback behavior is set on each effect:
effect.triggerType: 'once' | 'repeat' | 'alternate' | 'state'; // default: 'once'
```
**CRITICAL:** When source and target are the **same element**, MUST use `triggerType: 'once'`. For `'repeat'` / `'alternate'` / `'state'`, ALWAYS use **separate** source and target elements — animating the observed element can cause it to leave/re-enter the viewport, causing rapid re-triggers.
### viewProgress
Scroll-driven animations using native `ViewTimeline`, with polyfill where not supported. Progress is driven by scroll position. Control the range via `rangeStart`/`rangeEnd` on the effect (see [Scroll / Pointer-driven Effect](#scroll--pointer-driven-effect)).
`viewProgress` has no trigger params. Range configuration (`rangeStart`/`rangeEnd`) is on the effect, not on the trigger.
**CRITICAL:** Replace ALL `overflow: hidden` with `overflow: clip` on every element between the trigger source and the scroll container. `overflow: hidden` creates a new scroll context that breaks ViewTimeline. In Tailwind replace `overflow-hidden` with `overflow-clip`.
### pointerMove
```ts
params: {
hitArea?: 'self' | 'root'; // 'self' = source element bounds, 'root' = viewport
axis?: 'x' | 'y'; // restricts tracking to a single axis (for keyframeEffect)
}
```
**Rules:**
- Source element MUST NOT have `pointer-events: none`.
- MUST NOT use the same element as both source and target with size or position effects — use `selector` to target a child or set a different `key`.
- Use a `(hover: hover)` media condition to disable on touch-only devices. On touch-only devices prefer `viewEnter` or `viewProgress` fallbacks.
- For 2D effects, use `namedEffect` mouse presets or `customEffect`. `keyframeEffect` only supports a single axis.
- For independent 2-axis control with keyframes, use two separate interactions (one `axis: 'x'`, one `axis: 'y'`) with `composite: 'add'` or `'accumulate'` on the second effect.
**`centeredToTarget`** — set `true` to remap the `0–1` progress range so that `0.5` progress corresponds to the center of the target element. Use when source and target are different elements, or when `hitArea: 'root'` is used, so that the pointer resting over the target center produces 50% progress regardless of position in viewport.
**Progress object** (for `customEffect`):
```ts
{ x: number; y: number; v?: { x: number; y: number }; active?: boolean }
// x, y: 0–1 normalized position within hit area
// v: velocity vector (unbounded, typically -1 to 1 range at moderate speed; 0 = stationary)
// active: whether pointer is within the active hit area
```
### animationEnd
```ts
params: {
effectId: string;
} // the effect to wait for
```
Fires when the specified effect completes on the source element. Useful for chaining sequences.
---
## Effects
Each effect applies a visual change to a target element. An effect is either inline or referenced by `effectId` from the top-level `effects` registry (`EffectRef`). An `EffectRef` inherits all properties from the registry entry, and can override any of them (e.g. `key`, `duration`, `easing`, `fill`, etc.) — not just the target. See [Element Resolution](#element-resolution) for how the target is determined.
### Common fields
```ts
{
key?: string; // target element key; omit to target the source
effectId?: string; // reference to effects registry (EffectRef)
conditions?: string[]; // ids referencing the top-level conditions map; all must pass
selector?: string; // optional — CSS selector to refine target element
listContainer?: string; // optional — CSS selector for list container
listItemSelector?: string; // optional — filter which children of listContainer are selected
composite?: 'replace' | 'add' | 'accumulate';
fill?: 'none' | 'forwards' | 'backwards' | 'both';
}
```
**`fill` guidance:**
- `'both'` — use for scroll-driven (`viewProgress`), pointer-driven (`pointerMove`), and toggling effects (`hover`/`click` with `alternate`, `repeat`, or `state` type).
- `'backwards'` — use for entrance animations with `type: 'once'` when the element's own CSS already matches the final keyframe (applies the initial keyframe during any `delay`).
**`composite`** — same as CSS's `animation-composition`. Controls how this effect combines with others on the same property (transforms & filters):
- `'replace'` (default): fully replaces prior values.
- `'add'`: concatenates transform/filter functions after any existing ones (e.g. existing `translateX(10px)` + added `translateY(20px)` → both apply).
- `'accumulate'`: merges arguments of matching functions (e.g. `translateX(10px)` + `translateX(20px)` → `translateX(30px)`); non-matching functions concatenate like `'add'`.
**`easing` guidance:** from `@wix/motion` (in addition to standard CSS easings):
`'linear'`, `'ease'`, `'ease-in'`, `'ease-out'`, `'ease-in-out'`, `'sineIn'`, `'sineOut'`, `'sineInOut'`, `'quadIn'`, `'quadOut'`, `'quadInOut'`, `'cubicIn'`, `'cubicOut'`, `'cubicInOut'`, `'quartIn'`, `'quartOut'`, `'quartInOut'`, `'quintIn'`, `'quintOut'`, `'quintInOut'`, `'expoIn'`, `'expoOut'`, `'expoInOut'`, `'circIn'`, `'circOut'`, `'circInOut'`, `'backIn'`, `'backOut'`, `'backInOut'`, or any `'cubic-bezier(...)'` / `'linear(...)'` string.
### Time-based Effect
Used with `hover`, `click`, `viewEnter`, `animationEnd` triggers.
```ts
{
duration: number; // REQUIRED (ms)
easing?: string; // CSS easing or named easing (see below)
delay?: number; // ms
iterations?: number; // >=1 or Infinity; 0 is treated as Infinity
alternate?: boolean;
reversed?: boolean;
fill?: 'none' | 'forwards' | 'backwards' | 'both';
composite?: 'replace' | 'add' | 'accumulate';
// + exactly one animation payload (see below)
}
```
### Scroll / Pointer-driven Effect
Used with `viewProgress` and `pointerMove` triggers.
```ts
{
rangeStart?: RangeOffset; // REQUIRED for viewProgress
rangeEnd?: RangeOffset; // REQUIRED for viewProgress
easing?: string; // CSS easing or named easing (see above)
iterations?: number; // NOT Infinity
alternate?: boolean;
reversed?: boolean;
fill?: 'none' | 'forwards' | 'backwards' | 'both';
composite?: 'replace' | 'add' | 'accumulate';
centeredToTarget?: boolean;
transitionDuration?: number; // ms, smoothing on progress jumps (primarily for pointerMove)
transitionDelay?: number; // ms (primarily for pointerMove)
transitionEasing?: 'linear' | 'hardBackOut' | 'easeOut' | 'elastic' | 'bounce';
// + exactly one animation payload (see below)
}
```
**RangeOffset** — works like CSS's `animation-range`:
```ts
{
name?: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing';
offset?: { value: number; unit: 'percentage' | 'px' | 'vh' | 'vw' }
}
```
| Range name | Meaning |
| :--------------- | :------------------------------------------------------------- |
| `entry` | Element entering viewport |
| `exit` | Element exiting viewport |
| `contain` | After `entry` range and before `exit` range |
| `cover` | Full range from `entry` through `contain` and `exit` |
| `entry-crossing` | From element's leading edge entering to trailing edge entering |
| `exit-crossing` | From element's leading edge exiting to trailing edge exiting |
**Sticky container pattern** — for scroll-driven animations inside a stuck `position: sticky` container:
- Tall wrapper: height defines scroll distance (e.g. `300vh` for ~2 viewport-heights of scroll travel).
- Sticky child (`key`) with `position: sticky; top: 0; height: 100vh`: stays fixed while the wrapper scrolls. This is the ViewTimeline source.
- Use `rangeStart/rangeEnd` with `name: 'contain'` to animate only during the stuck phase.
### StateEffect (CSS style toggle)
Used with `hover` / `click` triggers. Set `stateAction` on the effect to control state behavior.
**StateEffect** (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.
```ts
// Shared timing for all properties:
{
transition: {
duration?: number; delay?: number; easing?: string;
styleProperties: [{ name: string; value: string }]
}
}
// Per-property timing:
{
transitionProperties: [
{ name: string; value: string; duration?: number; delay?: number; easing?: string }
]
}
```
CSS property names use **camelCase** (e.g. `'backgroundColor'`, `'borderRadius'`).
### Animation Payloads
Exactly one MUST be provided per time-based or scroll/pointer-driven effect:
1. **`namedEffect`** (preferred) — pre-built presets from `@wix/motion-presets`. GPU-friendly and tuned.
```ts
namedEffect: {
type: '[PRESET_NAME]',
// ...optional [PRESET_OPTIONS] as additional properties
}
```
- `[PRESET_NAME]` — one of the registered preset names (see table below).
- `[PRESET_OPTIONS]` — optional preset-specific properties spread as additional keys on the object. **CRITICAL:** Do NOT guess option names/types. Omit unknown options and rely on defaults.
Available presets:
| Category | Presets |
| :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Entrance | `FadeIn`, `GlideIn`, `SlideIn`, `FloatIn`, `RevealIn`, `ExpandIn`, `BlurIn`, `FlipIn`, `ArcIn`, `ShuttersIn`, `CurveIn`, `DropIn`, `FoldIn`, `ShapeIn`, `TiltIn`, `WinkIn`, `SpinIn`, `TurnIn`, `BounceIn` |
| Ongoing | `Pulse`, `Spin`, `Breathe`, `Bounce`, `Wiggle`, `Flash`, `Flip`, `Fold`, `Jello`, `Poke`, `Rubber`, `Swing`, `Cross` |
| Scroll | `FadeScroll`, `RevealScroll`, `ParallaxScroll`, `MoveScroll`, `SlideScroll`, `GrowScroll`, `ShrinkScroll`, `TiltScroll`, `PanScroll`, `BlurScroll`, `FlipScroll`, `SpinScroll`, `ArcScroll`, `ShapeScroll`, `ShuttersScroll`, `SkewPanScroll`, `Spin3dScroll`, `StretchScroll`, `TurnScroll` |
| Mouse | `TrackMouse`, `Tilt3DMouse`, `Track3DMouse`, `SwivelMouse`, `AiryMouse`, `ScaleMouse`, `BlurMouse`, `SkewMouse`, `BlobMouse` |
- **CRITICAL** — Scroll presets (`*Scroll`) used with `viewProgress` MUST include `range` in options: `'in'` (ends at idle state), `'out'` (starts from idle state), or `'continuous'` (passes through idle). Prefer `'continuous'`.
- Mouse presets are preferred over `keyframeEffect` for `pointerMove` 2D effects.
2. **`keyframeEffect`** — custom keyframe animations.
```ts
keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [KEYFRAMES] }
```
- `[EFFECT_NAME]` — unique string identifier for this effect.
- `[KEYFRAMES]` — array of keyframe objects using standard WAAPI format (e.g. `[{ opacity: '0' }, { opacity: '1' }]`). Property names in camelCase.
3. **`customEffect`** — imperative update callback. Use only when CSS-based effects cannot express the desired behavior (e.g., animating SVG attributes, canvas, text content).
```ts
customEffect: [CUSTOM_EFFECT_CALLBACK];
```
- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: Element, progress: number | ProgressObject) => void`. Called on each animation frame.
---
## Sequences
Coordinate multiple effects with staggered timing. Prefer sequences over manual delay stagger.
### Sequence As type
```ts
{
effects: (Effect | EffectRef)[]; // REQUIRED
delay?: number; // ms before sequence starts
offset?: number; // ms between each child's animation start
offsetEasing?: string; // easing curve for staggering offsets
sequenceId?: string; // for caching/referencing
conditions?: string[]; // ids referencing the top-level conditions map
}
```
### Template
```ts
{
interactions: [
{
key: '[SOURCE_KEY]',
trigger: '[TRIGGER]',
params: [TRIGGER_PARAMS],
sequences: [
{
offset: [OFFSET_MS], // optional
offsetEasing: '[OFFSET_EASING]', // optional
delay: [DELAY_MS], // optional
effects: [
// if used `listContainer` each item in the list is a target of a child effect
{
effectId: '[EFFECT_ID]',
listContainer: '[LIST_CONTAINER_SELECTOR]',
},
// if multiple effects are given each generated effect is added to the sequence
],
},
],
},
],
effects: {
'[EFFECT_ID]': {
// effect definition (namedEffect, keyframeEffect, or customEffect)
},
},
}
```
### Variables
- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for /vanilla, `interactKey` for React).
- `[TRIGGER]` — any trigger for time-based animation effects (e.g., `'viewEnter'`, `'activate'`, `'interest'`).
- `[TRIGGER_PARAMS]` — trigger-specific parameters (e.g., `{ type: 'once', threshold: 0.3 }`).
- `[OFFSET_MS]` — ms between each child's animation start.
- `[OFFSET_EASING]` — CSS easing string or named easing from `@wix/motion`.
- `[DELAY_MS]` — optional. Base delay (ms) before the entire sequence starts.
- `[EFFECT_ID]` — string key referencing an entry in the top-level `effects` map.
- `[LIST_CONTAINER_SELECTOR]` — optional. CSS selector for the container whose children will be staggered.
Reusable sequences can be defined in `InteractConfig.sequences` and referenced by `sequenceId`.
---
## Conditions
Named conditions that gate interactions, effects, or sequences.
| Type | Predicate |
| :--------- | :------------------------------------------------------------------------ |
| `media` | CSS media query condition without `@media` (e.g., `'(min-width: 768px)'`) |
| `selector` | CSS selector; `&` is replaced with the base element selector |
Attach via `conditions: ['[CONDITION_ID]']` on interactions, effects, or sequences. On an interaction, conditions gate the entire trigger; on an effect, only that specific effect is skipped. All listed conditions must pass.
### Examples
```ts
conditions: {
'desktop': { type: 'media', predicate: '(min-width: 768px)' },
'hover-device': { type: 'media', predicate: '(hover: hover)' },
'reduced-motion': { type: 'media', predicate: '(prefers-reduced-motion: reduce)' },
'odd-items': { type: 'selector', predicate: ':nth-of-type(odd)' },
}
```
---
## CSS Generation & FOUC Prevention
### Generating CSS
Call `generate(config, useFirstChild)` server-side or at build time to produce complete CSS for **all** interactions in the config — including initial state for entrance effects triggered by `viewEnter`. The output includes `@keyframes`, animation/transition custom properties, `view-timeline` declarations, state-selector rules, coordinated-list aggregation, and FOUC-prevention initial rules.
The `useFirstChild` argument is a boolean flag which tells Interact whether to render `:first-child` selectors when using custom element (`web`) integration.
```ts
import { generate } from '@wix/interact/web';
const css = generate(config);
```
Inject the result into the `` (preferred), or beginning of ``, so it loads before the page content is painted:
```html
```
### FOUC Prevention (viewEnter + once)
**Problem:** Elements with entrance animations (e.g. `viewEnter` + `triggerType: 'once'` with `FadeIn`) start in their final visible state. Before the animation framework initializes and applies the starting keyframe (e.g. `opacity: 0`), the element is briefly visible at full opacity — causing a flash of unstyled/un-animated content (FOUC).
**Solution:** Two things are required — both MUST be present:
1. **Generate CSS** using `generate(config, useFirstChild)` — among all the rules it produces, it includes initial rules that hide entrance-animated elements from the moment the page renders.
2. **Mark elements with `initial`** — tells the runtime which elements have critical CSS applied so it can coordinate with the generated styles.
**Step 1: Generate CSS** — see above.
**Step 2: Mark elements**
**Web (Custom Elements):**
```html
```
**React:**
```tsx
...
```
**Vanilla:**
```html
```
### Scroll-driven CSS (viewProgress)
For `viewProgress` interactions, `generate()` emits `view-timeline` declarations and `animation-timeline`/`animation-range` properties.
No `initial` attribute is needed for scroll-driven animations.
### Rules
- `generate()` should be called server-side or at build time. Can also be called on client-side if page content is initially hidden (e.g. behind a loader/splash screen).
- `initial` is only valid for `viewEnter` + `triggerType: 'once'` where source and target are the same element.
- For `repeat`/`alternate`/`state`, do NOT use `initial`. Instead, manually apply the initial keyframe as inline styles on the target element and use `fill: 'both'`.
---
## Element Resolution
For simple use cases, `key` on the interaction matches the element, and the same element is both trigger source and animation target. The fields below are only needed for advanced patterns (lists, delegated triggers, child targeting).
### Source element resolution (Interaction level)
The source element is what the trigger attaches to. Resolved in priority order:
1. **`listContainer` + `listItemSelector`** — trigger attaches to each element matching `listItemSelector` within the `listContainer`. Use `listItemSelector` only when you need to **filter** which children participate (e.g. select only `.active` items). If all immediate children should participate, omit `listItemSelector`.
2. **`listContainer` only** — trigger attaches to each immediate child of the container. This is the common case for lists.
3. **`listContainer` + `selector`** — trigger attaches to the element found via `querySelector` within each immediate child of the container.
4. **`selector` only** — trigger attaches to all elements matching `querySelectorAll` within the root ``.
5. **Fallback** — first child of `` (web) or the root element (react/vanilla).
### Target element resolution (Effect level)
The target element is what the effect animates. Resolved in priority order:
1. **`Effect.key`** — the `` with matching `data-interact-key`.
2. **Registry Effect's `key`** — if the effect is an `EffectRef`, the `key` from the referenced registry entry is used.
3. **Fallback to `Interaction.key`** — the same `key` is used for the source will be used for the target.
4. After resolving the root target, `selector`, `listContainer`, and `listItemSelector` on the effect further refine which child elements within that target are animated (same priority order as source resolution).
---
## Static API
| Method / Property | Description |
| :---------------------------------- | :------------------------------------------------------------------------------------------------------------ |
| `Interact.create(config)` | Initialize with a config. Returns the instance. Store the instance to manage its lifecycle. |
| `Interact.registerEffects(presets)` | Register named effect presets. MUST be called before `create`. |
| `Interact.destroy()` | Tear down all instances. Call on unmount or route change to prevent memory leaks. |
| `Interact.forceReducedMotion` | `boolean` (default: `false`) — force reduced-motion behavior regardless of OS setting. |
| `Interact.allowA11yTriggers` | `boolean` (default: `false`) — enable accessibility trigger variants (`interest`, `activate`). |
| `Interact.setup(options)` | Configure global options for scroll, pointer, and viewEnter systems. Call before `create`. See options below. |
**`Interact.setup(options)`** — optional configuration object:
| Option | Type | Description |
| :--------------------- | :----------------------------- | :-------------------------------------------------------------------- |
| `scrollOptionsGetter` | `() => Partial` | Function returning defaults for scroll-driven animation configuration |
| `pointerOptionsGetter` | `() => Partial` | Function returning defaults for pointer-move animation configuration |
| `viewEnter` | `Partial` | Defaults for all viewEnter triggers (`threshold`,`inset`) |
| `allowA11yTriggers` | `boolean` | Enable accessibility trigger variants (use `interest` and `activate`) |
Use `setup()` when you need to override default observer thresholds or provide global configuration that applies to all interactions of a given trigger type.
Each `Interact.create()` call returns an instance. Store instances and call `instance.destroy()` when no longer needed (e.g. on component unmount) to prevent stale listeners and memory leaks.
--- integration.md ---
# @wix/interact Integration Rules
Rules for integrating `@wix/interact` into a webpage — binding animations and effects to user-driven triggers via declarative configuration.
## Table of Contents
- [Entry Points](#entry-points)
- [Web (Custom Elements)](#web-custom-elements)
- [React](#react)
- [Vanilla JS](#vanilla-js)
- [Named Effects & registerEffects](#named-effects--registereffects)
- [Configuration Schema](#configuration-schema)
- [InteractConfig](#interactconfig)
- [Interaction](#interaction)
- [Element Selection](#element-selection)
- [Triggers](#triggers)
- [Sequences](#sequences)
- [CSS Generation & FOUC Prevention](#css-generation--fouc-prevention)
- [Static API](#static-api)
---
## Entry Points
Install with your package manager:
```bash
npm install @wix/interact @wix/motion-presets
```
### Web (Custom Elements)
```typescript
import { Interact } from '@wix/interact/web';
Interact.create(config);
```
The `config` object contains `interactions` (trigger-effect bindings), and optionally `effects`, `sequences`, and `conditions`. See [Configuration Schema](#configuration-schema) for full details.
Wrap target elements with ``:
```html
```
**Rules:**
- MUST set `data-interact-key` to a unique string within the page.
- MUST contain at least one child element (the library targets `.firstElementChild` by default).
### React
- Wrap the `Interact.create()` call in a `useEffect` hook to prevent it from running on server-side.
- Store the returned instance, and call its `.destroy()` method on the effect's cleanup function.
```typescript
import { useEffect } from 'react';
import { Interact } from '@wix/interact/react';
useEffect(() => {
const instance = Interact.create(config);
return () => {
instance.destroy();
};
}, [config]);
```
Replace target elements with ``:
```tsx
import { Interaction } from '@wix/interact/react';
...
;
```
**Rules:**
- MUST set `tagName` to a valid HTML tag string for the element being replaced.
- MUST set `interactKey` to a unique string within the page.
### Vanilla JS
```typescript
import { Interact } from '@wix/interact';
const interact = Interact.create(config);
interact.add(element, 'hero');
```
**Rules:**
- Call `add(element, key)` after elements exist in the DOM.
- Call `remove(key)` to unregister all interactions for a key.
---
## Named Effects & registerEffects
To use `namedEffect` presets from `@wix/motion-presets`, register them before calling `Interact.create`. For full effect type syntax (`keyframeEffect`, `customEffect`, `StateEffect`, `ScrubEffect`), see `full-lean.md`.
**Install:**
```bash
> npm install @wix/motion-presets
```
**Import and register:**
```typescript
import { Interact } from '@wix/interact/web';
import * as presets from '@wix/motion-presets';
Interact.registerEffects(presets);
```
Or register selectively:
```typescript
import { FadeIn, ParallaxScroll } from '@wix/motion-presets';
Interact.registerEffects({ FadeIn, ParallaxScroll });
```
Then use in effects:
```typescript
{ namedEffect: { type: 'FadeIn' }, duration: 800, easing: 'ease-out' }
```
For full effect type syntax (`keyframeEffect`, `namedEffect`, `customEffect`, `transition`/`transitionProperties`), see [full-lean.md](./full-lean.md) and the trigger-specific rule files.
---
## Configuration Schema
### InteractConfig
```typescript
type InteractConfig = {
interactions: Interaction[];
effects?: Record;
sequences?: Record;
conditions?: Record;
};
```
| Field | Description |
| :------------- | :---------------------------------------------------------------------- |
| `interactions` | Required. Array of interaction definitions binding triggers to effects. |
| `effects?` | Reusable effects referenced by `effectId` from interactions. |
| `sequences?` | Reusable sequence definitions, referenced by `sequenceId`. |
| `conditions?` | Named conditions (media/container/selector queries), referenced by ID. |
Each call to `Interact.create(config)` creates a new `Interact` instance. A single config can define multiple interactions.
### Interaction
```typescript
{
key: string; // REQUIRED — matches data-interact-key / interactKey
trigger: TriggerType; // REQUIRED — trigger type
params?: TriggerParams; // trigger-specific parameters
selector?: string; // CSS selector to refine target within the element
listContainer?: string; // CSS selector for a list container
listItemSelector?: string; // optional — CSS selector to filter which children of listContainer are selected
conditions?: string[]; // array of condition IDs; all must pass
effects?: Effect[]; // effects to apply
sequences?: SequenceConfig[]; // sequences to apply
}
```
At least one of `effects` or `sequences` MUST be provided.
**Multiple effects per interaction:** A single interaction can contain multiple effects in its `effects` array. All effects share the same trigger — they fire together when the trigger activates. Use this to animate different targets from the same trigger event instead of duplicating interactions.
### Element Selection
**Most common**: Omit `selector`/`listContainer`/`listItemSelector` entirely — the element with the matching key is used as both source and target. Use `selector` to target a child element within the keyed element. Use `listContainer` for staggered sequences across list items.
`listItemSelector` is **optional** — only use it when you need to **filter** which children of `listContainer` participate (e.g. select only `.active` items). When omitted, all immediate children of the `listContainer` are selected.
#### Source element resolution (Interaction level)
The source element is what the trigger attaches to. Resolved in priority order:
1. **`listContainer` + `listItemSelector`** — matches only the elements matching `listItemSelector` within the the `listContainer`.
2. **`listContainer` only** — trigger attaches to all immediate children of the container (common case).
3. **`listContainer` + `selector`** — matches via `querySelector` within each immediate child of the container.
4. **`selector` only** — matches via `querySelectorAll` within the root element.
5. **Fallback** — first child of `` (web) or the root element (react/vanilla).
#### Target element resolution (Effect level)
The target element is what the effect animates. Resolved in priority order:
1. **`Effect.key`** — the root 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's root.
4. After resolving the target's root, `selector`, `listContainer`, and `listItemSelector` on the effect further refine which child elements within that target are animated (same priority order as source resolution).
---
## Triggers
| Trigger | Description | Trigger `params` | Rules |
| :------------- | :------------------------------------- | :-------------------------------------------------------------------------------------- | :----------------------------------- |
| `hover` | Mouse enter/leave | No params. Set `triggerType` on TimeEffect or `stateAction` on StateEffect. | [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 | `threshold?`; `inset?`. Set `triggerType` on TimeEffect or sequence config. | [viewenter.md](./viewenter.md) |
| `viewProgress` | Scroll-driven (ViewTimeline) | No trigger params. Configure `rangeStart`/`rangeEnd` on the **effect**, not on `params` | [viewprogress.md](./viewprogress.md) |
| `pointerMove` | Mouse movement | `hitArea?`: `'self'` \| `'root'`; `axis?`: `'x'` \| `'y'` | [pointermove.md](./pointermove.md) |
| `animationEnd` | Chain after another effect | `effectId`: ID of the preceding effect | — |
For `hover`/`click` (and their accessible variants `interest`/`activate`): set `triggerType` on the effect for keyframe/named/custom effects (TimeEffect), or `stateAction` on the effect for transitions (StateEffect). Do not mix both on the same effect.
---
## Sequences
Sequences coordinate multiple effects with staggered timing.
```typescript
{
offset: number, // ms between consecutive items
offsetEasing: string, // Any valid easing string for stagger distribution curve
delay: number, // ms base delay before the sequence starts
effects: [
/* ... effect definitions */,
],
}
```
Define reusable sequences in `InteractConfig.sequences` and reference by `sequenceId`:
```typescript
{
sequences: {
'stagger-fade': {
/* ... sequence definition */
},
},
interactions: [
{
key: `'[SOURCE_KEY]'`,
trigger: `'[TRIGGER]'`,
params: `[TRIGGER_PARAMS]`,
sequences: [{ sequenceId: 'stagger-fade' }],
},
],
}
```
---
## CSS Generation & FOUC Prevention
`generate(config, useFirstChild)` produces complete CSS for **all** interactions in the config — `@keyframes`, animation/transition custom properties, `view-timeline` declarations, state-selector rules, coordinated-list aggregation, and FOUC-prevention initial rules. Call it server-side or at build time and inject the result into ``.
The `useFirstChild` argument is a boolean flag which tells Interact whether to render `:first-child` selectors when using custom element (`web`) integration.
```javascript
import { generate } from '@wix/interact/web';
const css = generate(config);
```
**Append to `` or beginning of ``:**
```html
```
### FOUC Prevention (viewEnter + once)
**Problem:** Elements with entrance animations (e.g. `FadeIn` on `viewEnter`) are initially visible in their final state. Before the animation framework applies the starting keyframe, the content flashes visibly — a flash of un-animated content (FOUC).
**Solution:** Two things are required — both MUST be present:
1. **Generate CSS** with `generate(config, useFirstChild)` — among all the rules it produces, it includes initial rules that hide entrance-animated elements until the animation starts.
2. **Mark elements with `initial`** — `data-interact-initial="true"` on ``, or `initial={true}` on `` in React.
Using only one of these has no effect — both are required.
See [viewenter.md](./viewenter.md) for full details.
**Rules:**
- `generate()` should be called server-side or at build time. Can also be called on the client if page content is initially hidden (e.g. behind a loader).
- `generate()` processes all interactions, not just `viewEnter`.
- `initial` is only valid for `viewEnter` + `triggerType: 'once'` (or no `triggerType`, which defaults to `'once'`) where source and target are the same element.
**Web:**
```html
```
**React:**
```tsx
...
```
**Vanilla:**
```html
```
---
## Static API
Each `Interact.create(config)` call returns an instance. Keep a reference if you need to add/remove elements dynamically (vanilla JS) or to destroy a specific instance. Call `Interact.destroy()` to tear down all instances at once (e.g. on page navigation).
| Method / Property | Description |
| :---------------------------------- | :------------------------------------------------------------------------------------------- |
| `Interact.create(config)` | Initialize with a config. Returns the instance. Multiple configs create separate instances. |
| `Interact.registerEffects(presets)` | Register named effect presets before `create`. Required for `namedEffect` usage. |
| `Interact.destroy()` | Tear down all instances. |
| `Interact.forceReducedMotion` | `boolean` — force reduced-motion behavior regardless of OS setting. Default: `false`. |
| `Interact.allowA11yTriggers` | `boolean` — enable accessibility triggers (`interest`, `activate`). Default: `false`. |
| `Interact.setup(options)` | Configure global defaults for scroll/pointer/viewEnter trigger params. Call before `create`. |
--- click.md ---
# Click Trigger Rules for @wix/interact
This document contains rules for generating click-triggered interactions in `@wix/interact`.
**CRITICAL — Accessible click**: Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space).
## Table of Contents
- [Rule 1: keyframeEffect / namedEffect (TimeEffect)](#rule-1-keyframeeffect--namedeffect-timeeffect)
- [Rule 2: transition / transitionProperties (StateEffect)](#rule-2-transition--transitionproperties-stateeffect)
- [Rule 3: customEffect (TimeEffect)](#rule-3-customeffect-timeeffect)
- [Rule 4: Sequences](#rule-4-sequences)
---
## Rule 1: keyframeEffect / namedEffect (TimeEffect)
Use `keyframeEffect` or `namedEffect` when the click should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior.
**CRITICAL:** Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'click',
effects: [
{
key: '[TARGET_KEY]',
triggerType: '[TRIGGER_TYPE]',
// --- pick ONE of the two effect types ---
keyframeEffect: {
name: '[EFFECT_NAME]',
keyframes: [KEYFRAMES],
},
// OR
namedEffect: [NAMED_EFFECT_DEFINITION],
fill: '[FILL_MODE]',
reversed: [INITIAL_REVERSED_BOOL],
duration: [DURATION_MS],
easing: '[EASING_FUNCTION]',
delay: [DELAY_MS],
iterations: [ITERATIONS],
alternate: [ALTERNATE_BOOL],
effectId: '[UNIQUE_EFFECT_ID]'
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). The element that listens for clicks.
- `[TARGET_KEY]` — identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.
- `[TRIGGER_TYPE]` — `triggerType` on the effect. One of:
- `'alternate'` — plays forward on first click, reverses on next click. Default.
- `'repeat'` — restarts the animation from the beginning on each click.
- `'once'` — plays once on the first click and never again.
- `'state'` — resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`).
- `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.
- `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`.
- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built, time-based animation effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.
- `[FILL_MODE]` - optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.
- `[INITIAL_REVERSED_BOOL]` — optional. `true` to start in the finished state so the entire effect is reversed.
- `[DURATION_MS]` — animation duration in milliseconds.
- `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`.
- `[DELAY_MS]` — optional delay before the effect starts, in milliseconds.
- `[ITERATIONS]` — optional. Number of iterations, or `Infinity` for continuous loops.
- `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration (within a single playback). Different from `triggerType: 'alternate'` which alternates per click.
- `[UNIQUE_EFFECT_ID]` — optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects from the top-level `effects` map.
---
## Rule 2: transition / transitionProperties (StateEffect)
Use `transition` or `transitionProperties` when the click should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Uses the `transition` CSS property. Set `stateAction` on the effect to control how the style is applied.
Use `transition` when all properties share timing. Use `transitionProperties` when each property needs independent `duration`, `delay`, or `easing`.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'click',
effects: [
{
key: '[TARGET_KEY]',
stateAction: '[STATE_ACTION]',
// --- pick ONE of the two transition forms ---
transition: {
duration: [DURATION_MS],
delay: [DELAY_MS],
easing: '[EASING_FUNCTION]',
styleProperties: [
{ name: '[CSS_PROP]', value: '[VALUE]' },
// ... more properties
]
},
// OR (when each property needs its own timing)
transitionProperties: [
{
name: '[CSS_PROP]',
value: '[VALUE]',
duration: [DURATION_MS],
delay: [DELAY_MS],
easing: '[EASING_FUNCTION]'
},
// ... more properties
]
}
]
}
```
### Variables
- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1.
- `[STATE_ACTION]` — `stateAction` on the effect. One of:
- `'toggle'` — applies the style state, removes it on next click. Default.
- `'add'` — applies the style state. Does not remove on subsequent clicks.
- `'remove'` — removes a previously applied style state.
- `'clear'` — clears all previously applied style states. Useful for resetting multiple stacked style states at once.
- `[CSS_PROP]` — CSS property name as a string in camelCase format (e.g. `'backgroundColor'`, `'borderRadius'`, `'opacity'`).
- `[VALUE]` — target CSS value for the property.
- `[DURATION_MS]` — transition duration in milliseconds.
- `[DELAY_MS]` — optional transition delay in milliseconds.
- `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`.
---
## Rule 3: customEffect (TimeEffect)
Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation, randomized behavior). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'click',
effects: [
{
key: '[TARGET_KEY]',
triggerType: '[TRIGGER_TYPE]',
customEffect: [CUSTOM_EFFECT_CALLBACK],
duration: [DURATION_MS],
easing: '[EASING_FUNCTION]'
}
]
}
```
### Variables
- `[SOURCE_KEY]` / `[TARGET_KEY]` / `[TRIGGER_TYPE]` — same as Rule 1.
- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with target element and `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. Set `triggerType` on the sequence config to control playback behavior.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'click',
sequences: [
{
triggerType: '[TRIGGER_TYPE]',
offset: [OFFSET_MS],
offsetEasing: '[OFFSET_EASING]',
effects: [
[EFFECT_DEFINTION],
// .. more effects as necessary
]
}
]
}
```
### Variables
- `[SOURCE_KEY]` / `[TRIGGER_TYPE]` — same as Rule 1. `triggerType` is set on the sequence config, not on individual effects within the sequence.
- `[OFFSET_MS]` — time offset for staggering each child's animation start, in milliseconds.
- `[OFFSET_EASING]` — easing curve for the offset staggering distribution. Defaults to `'linear'`.
- `[EFFECT_DEFINTION]` — a definition of, or a reference to a time-based animation effect.
--- hover.md ---
# Hover Trigger Rules for @wix/interact
This document contains rules for generating hover-triggered interactions in `@wix/interact`.
**CRITICAL — Accessible hover**: Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus.
- **CRITICAL**: MUST AVOID using the same element as both trigger source and effect target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation.
## Table of Contents
- [Rule 1: keyframeEffect / namedEffect (TimeEffect)](#rule-1-keyframeeffect--namedeffect-timeeffect)
- [Rule 2: transition / transitionProperties (StateEffect)](#rule-2-transition--transitionproperties-stateeffect)
- [Rule 3: customEffect (TimeEffect)](#rule-3-customeffect-timeeffect)
- [Rule 4: Sequences](#rule-4-sequences)
---
## Rule 1: keyframeEffect / namedEffect (TimeEffect)
Use `keyframeEffect` or `namedEffect` when the hover should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior.
**CRITICAL:** Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.
**Multiple effects:** The `effects` array can contain multiple effects — all share the same hover trigger and fire together. Use this to animate different targets from a single hover event.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'hover',
effects: [
{
key: '[TARGET_KEY]',
triggerType: '[TRIGGER_TYPE]',
// --- pick ONE of the two effect types ---
keyframeEffect: {
name: '[EFFECT_NAME]',
keyframes: [KEYFRAMES],
},
// OR
namedEffect: [NAMED_EFFECT_DEFINITION],
fill: '[FILL_MODE]',
duration: [DURATION_MS],
easing: '[EASING_FUNCTION]',
delay: [DELAY_MS],
iterations: [ITERATIONS],
alternate: [ALTERNATE_BOOL]
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). The element that listens for hover.
- `[TARGET_KEY]` — identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).
- `[TRIGGER_TYPE]` — `triggerType` on the effect. One of:
- `'alternate'` — plays forward on enter, reverses on leave. Default. Most common for hover.
- `'repeat'` — restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.
- `'once'` — plays once on the first enter and never again.
- `'state'` — resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`).
- `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.
- `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`.
- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.
- `[DURATION_MS]` — animation duration in milliseconds.
- `[EASING_FUNCTION]` — CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.
- `[DELAY_MS]` — optional delay before the effect starts, in milliseconds.
- `[ITERATIONS]` — optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.
- `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration (within a single playback).
- `[FILL_MODE]` — usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.
---
## Rule 2: transition / transitionProperties (StateEffect)
Use `transition` or `transitionProperties` when the hover should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Set `stateAction` on the effect to control how the style is applied.
Use `transition` when all properties share timing. Use `transitionProperties` when each property needs independent `duration`, `delay`, or `easing`.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'hover',
effects: [
{
key: '[TARGET_KEY]',
stateAction: '[STATE_ACTION]',
// --- pick ONE of the two transition forms ---
transition: {
duration: [DURATION_MS],
delay: [DELAY_MS],
easing: '[EASING_FUNCTION]',
styleProperties: [
{ name: '[CSS_PROP]', value: '[VALUE]' },
// ... more properties
]
},
// OR (when each property needs its own timing)
transitionProperties: [
{
name: '[CSS_PROP]',
value: '[VALUE]',
duration: [DURATION_MS],
delay: [DELAY_MS],
easing: '[EASING_FUNCTION]'
},
// ... more properties
]
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1.
- `[STATE_ACTION]` — `stateAction` on the effect. One of:
- `'toggle'` — applies the style state on enter, removes on leave. Default.
- `'add'` — applies the style state on enter. Leave does NOT remove it.
- `'remove'` — removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`.
- `'clear'` — clears all previously applied style states on enter. Use to reset multiple stacked `'add'` style changes at once (e.g. a "reset" hover area that undoes several accumulated states).
- `[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`.
---
## Rule 3: customEffect (TimeEffect)
Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'hover',
effects: [
{
key: '[TARGET_KEY]',
triggerType: '[TRIGGER_TYPE]',
customEffect: [CUSTOM_EFFECT_CALLBACK],
duration: [DURATION_MS],
easing: '[EASING_FUNCTION]'
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[SOURCE_KEY]` / `[TARGET_KEY]` / `[TRIGGER_TYPE]` — same as Rule 1.
- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(target: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `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. Set `triggerType` on the sequence config to control playback behavior.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'hover',
sequences: [
{
triggerType: '[TRIGGER_TYPE]',
offset: [OFFSET_MS],
offsetEasing: '[OFFSET_EASING]',
effects: [
[EFFECT_DEFINTION],
// .. more effects as necessary
]
}
]
}
```
### Variables
- `[SOURCE_KEY]` / `[TRIGGER_TYPE]` — same as Rule 1. `triggerType` is set on the sequence config, not on individual effects within the sequence.
- `[OFFSET_MS]` — time offset for staggering each child's animation start, in milliseconds.
- `[OFFSET_EASING]` — easing curve for the offset staggering distribution. CSS easing string, or named easing from `@wix/motion`. Defaults to `'linear'`.
- `[EFFECT_DEFINTION]` — a definition of or a reference to a time-based animation effect.
--- pointermove.md ---
# PointerMove Trigger Rules for @wix/interact
These rules help generate pointer-driven interactions using `@wix/interact`. PointerMove triggers create real-time animations that respond to mouse movement over elements or the entire viewport.
## 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:
- The source element **MUST NOT** have `pointer-events: none` — it needs to receive pointer events.
- **CRITICAL**: MUST AVOID using the same element as both source and target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation.
---
## PointerMoveParams
`params` object for `pointerMove` interactions:
```typescript
type PointerMoveParams = {
hitArea?: 'root' | 'self';
axis?: 'x' | 'y';
};
```
### Properties
- `hitArea` — determines where mouse movement is tracked:
- `'self'` — tracks pointer within the source element's bounds only. Use for local pointer-tracking effects on a specific element.
- `'root'` — tracks pointer anywhere in the viewport. Use for global cursor followers, ambient effects.
- `axis` — restricts pointer tracking to a single axis. Used with `keyframeEffect` to map one axis to 0–1 progress; ignored by `namedEffect` and `customEffect` which receive the full 2D progress:
- `'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.
- For `namedEffect` or `customEffect` both axes are available via the 2D progress object, and will be ignored.
---
## Progress Object Structure
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?: {
x: number; // Horizontal velocity: negative = moving left, positive = moving right. Magnitude reflects speed.
y: number; // Vertical velocity: negative = moving up, positive = moving down. Magnitude reflects speed.
};
active?: boolean; // Whether mouse is currently in the hit area
};
```
---
## Centering with `centeredToTarget`
Controls which element's bounds define the 0–1 progress range.
- **`false` (default)**: Progress is calculated against the **source element's** (or viewport's) bounds. The `50%` progress of the timeline is at the center of the source element.
- **`true`**: `50%` progress of the timeline is calculated against the **target element's center**. The edges of the timeline are still calculated against the edges of the source element/viewport depending on `hitAea`.
---
## Device Conditions
`pointerMove` works best on hover-capable devices. Use a `conditions` entry with a `(hover: hover)` media query to prevent the interaction from registering on touch-only devices. On touch-only devices, consider a fallback to `viewEnter` or `viewProgress` based interactions:
```typescript
{
conditions: {
'[CONDITION_NAME]': { type: 'media', predicate: '(hover: hover)' }
},
interactions: [
{
key: '[SOURCE_KEY]',
trigger: 'pointerMove',
conditions: ['[CONDITION_NAME]'],
params: { hitArea: '[HIT_AREA]' },
effects: [ /* ... */ ]
}
]
}
```
For devices with dynamic viewport sizes (e.g. mobile browsers where the address bar collapses), consider using viewport-relative units carefully and prefer `lvh`/`svh` over `dvh` unless dynamic viewport behavior is specifically desired.
---
## Rule 1: namedEffect
Use pre-built mouse presets from `@wix/motion-presets` that handle 2D mouse tracking internally. Mouse presets are preferred over `keyframeEffect` for 2D effects.
**Multiple effects:** The `effects` array can contain multiple effects — all share the same pointer tracking and fire together. Use this to animate different targets from the same pointer movement.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'pointerMove',
params: {
hitArea: '[HIT_AREA]'
},
effects: [
{
key: '[TARGET_KEY]',
namedEffect: {
type: '[NAMED_EFFECT_TYPE]',
[EFFECT_PROPERTIES]
},
centeredToTarget: [CENTERED_TO_TARGET],
transitionDuration: [TRANSITION_DURATION_MS],
transitionEasing: '[TRANSITION_EASING]'
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). The element that tracks pointer movement.
- `[TARGET_KEY]` — identifier matching the element's key on the element to animate (can be same as source or different).
- `[HIT_AREA]` — `'self'` (track pointer within source element) or `'root'` (track pointer anywhere in viewport).
- `[NAMED_EFFECT_TYPE]` — a registered effect name, or a preset from `@wix/motion-presets` `mouse` library.
- `[EFFECT_PROPERTIES]` — preset-specific options. Refer to motion-presets rules for each preset's available options and their value types. Do NOT guess preset option names or types; omit unknown options and rely on defaults.
- `[CENTERED_TO_TARGET]` — `true` or `false`. See **Centering with `centeredToTarget`** above.
- `[TRANSITION_DURATION_MS]` — optional number. Milliseconds for smoothing (interpolating) between progress updates. The animation does not jump to the new progress value instantly; instead it transitions over this duration. Use to add inertia/lag to the effect, making it feel more physical (e.g. `200`–`600`).
- `[TRANSITION_EASING]` — optional string. CSS easing or named easing from `@wix/motion`. Adds a natural deceleration feel when used with `transitionDuration`.
---
## Rule 2: keyframeEffect with Single Axis
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
{
key: '[SOURCE_KEY]',
trigger: 'pointerMove',
params: {
hitArea: '[HIT_AREA]',
axis: '[AXIS]'
},
effects: [
{
key: '[TARGET_KEY]',
keyframeEffect: {
name: '[EFFECT_NAME]',
keyframes: [KEYFRAMES]
},
fill: 'both',
centeredToTarget: [CENTERED_TO_TARGET],
transitionDuration: [TRANSITION_DURATION_MS],
transitionEasing: '[TRANSITION_EASING]',
effectId: '[UNIQUE_EFFECT_ID]'
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1.
- `[HIT_AREA]` — `'self'` or `'root'`.
- `[AXIS]` — `'x'` (horizontal) or `'y'` (vertical). Defaults to `'y'` when omitted.
- `[EFFECT_NAME]` — unique string name for the keyframe effect.
- `[KEYFRAMES]` — array of CSS keyframe objects (e.g. `[{ transform: 'rotate(-10deg)' }, { transform: 'rotate(0)' }, { transform: 'rotate(10deg)' }]`). Distributed evenly across 0–1 progress: first keyframe = progress 0 (left/top edge), last = progress 1 (right/bottom edge). Any number of keyframes is allowed.
- `[CENTERED_TO_TARGET]` — optional. `true` or `false`. See **Centering with `centeredToTarget`** above. Defaults to `false`.
- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details.
- `[TRANSITION_EASING]` — optional. CSS easing string or named easing from `@wix/motion`. See Rule 1 for supported values.
- `[UNIQUE_EFFECT_ID]` — optional string identifier.
---
## Rule 3: Two keyframeEffects with Two Axes and `composite`
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: '[SOURCE_KEY]',
trigger: 'pointerMove',
params: { hitArea: '[HIT_AREA]', axis: 'x' },
effects: [{ key: '[TARGET_KEY]', effectId: '[X_EFFECT_ID]' }]
},
{
key: '[SOURCE_KEY]',
trigger: 'pointerMove',
params: { hitArea: '[HIT_AREA]', axis: 'y' },
effects: [{ key: '[TARGET_KEY]', effectId: '[Y_EFFECT_ID]' }]
}
],
effects: {
'[X_EFFECT_ID]': {
keyframeEffect: {
name: '[X_EFFECT_NAME]',
keyframes: [X_KEYFRAMES]
},
fill: '[FILL_MODE]', // usually 'both'
composite: '[COMPOSITE_OPERATION]',
transitionDuration: [TRANSITION_DURATION_MS],
transitionEasing: '[TRANSITION_EASING]'
},
'[Y_EFFECT_ID]': {
keyframeEffect: {
name: '[Y_EFFECT_NAME]',
keyframes: [Y_KEYFRAMES]
},
fill: '[FILL_MODE]', // usually 'both'
composite: '[COMPOSITE_OPERATION]',
transitionDuration: [TRANSITION_DURATION_MS],
transitionEasing: '[TRANSITION_EASING]'
}
}
}
```
### Variables
- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1.
- `[HIT_AREA]` — `'self'` or `'root'`.
- `[X_EFFECT_ID]` / `[Y_EFFECT_ID]` — unique string identifiers for the X-axis and Y-axis effects. Required — they map to keys in the top-level `effects` map.
- `[X_EFFECT_NAME]` / `[Y_EFFECT_NAME]` — unique string names for each keyframe effect.
- `[X_KEYFRAMES]` / `[Y_KEYFRAMES]` — arrays of WAAPI keyframe objects for the X-axis and Y-axis effects respectively. Each effect can vary in propertise and keyframes.
- `[COMPOSITE_OPERATION]` — `'add'` or `'accumulate'`. Required when both effects animate `transform` and/or both animate `filter`, so their values combine rather than override. `'add'`: composited transform functions are appended. `'accumulate'`: matching function arguments are summed.
- `[FILL_MODE]` — typically `'both'` to ensure the effect keeps applying after exiting the effect's active range.
- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details.
- `[TRANSITION_EASING]` — optional. CSS easing function for the smoothing transition. See Rule 1 for supported values.
---
## Rule 4: customEffect
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: '[SOURCE_KEY]',
trigger: 'pointerMove',
params: {
hitArea: '[HIT_AREA]'
},
effects: [
{
key: '[TARGET_KEY]',
customEffect: (element: Element, progress: Progress) => {
[CUSTOM_ANIMATION_LOGIC]
},
centeredToTarget: [CENTERED_TO_TARGET],
transitionDuration: [TRANSITION_DURATION_MS],
transitionEasing: '[TRANSITION_EASING]'
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[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. See **Progress Object Structure** above.
- `[CENTERED_TO_TARGET]` — optional. `true` or `false`. See **Centering with `centeredToTarget`** above. Defaults to `false`.
- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details.
- `[TRANSITION_EASING]` — optional. CSS easing function for the smoothing transition. See Rule 1 for supported values.
--- viewenter.md ---
# ViewEnter Trigger Rules for @wix/interact
This document contains rules for generating interactions that respond to elements entering the viewport using the `@wix/interact`. ViewEnter triggers use IntersectionObserver to detect when elements become visible and are ideal for entrance animations, content reveals, and lazy-loading effects.
---
> **CRITICAL:** When the source (trigger) and target (effect) elements are the **same element**, use ONLY `triggerType: '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 (TimeEffect)](#rule-1-keyframeeffect--namedeffect-timeeffect)
- [Rule 2: customEffect (TimeEffect)](#rule-2-customeffect-timeeffect)
- [Rule 3: Sequences](#rule-3-sequences)
---
## Preventing Flash of Unstyled Content (FOUC)
> **Note:** `generate(config)` produces complete CSS for **all** interactions in the config — `@keyframes`, animation/transition properties, scroll-driven timelines, state effects, and more — not only the FOUC-prevention rules described here. The generated CSS uses attribute selectors, so animations bind reactively as elements appear in the DOM without JS-managed DOM references. See the [generate() documentation](../docs/api/functions.md#generate) for the full scope.
**Problem:** Elements with entrance animations (e.g. `FadeIn`) start in their final visible state (e.g. `opacity: 1`). Before the animation framework initializes and applies the starting keyframe (e.g. `opacity: 0`), the element is briefly visible at full opacity — a flash of un-animated content.
**Solution:** Two things are required — **both** MUST be present for FOUC prevention to work:
1. **Generate CSS** using `generate(config)` — among all the CSS it produces, it includes initial rules that hide entrance-animated elements from the moment the page renders, before JavaScript runs.
2. **Mark elements with `initial`** — set `data-interact-initial="true"` on ``, or `initial={true}` on the `` React component. This tells the runtime which elements have critical CSS applied.
If only one of these is present, FOUC prevention will **not** work. Both the CSS and the `initial` attribute are required.
### Step 1: Generate CSS and inject into `` (preferred), or beginning of ``
Call `generate(config)` server-side or at build time. Inject the resulting CSS into the document `` (or in `` before your content) so it loads before the page content is painted:
```typescript
import { generate } from '@wix/interact';
const config: InteractConfig = {
interactions: [
{
key: '[SOURCE_KEY]',
trigger: 'viewEnter',
params: {
threshold: [VIEW_TRIGGER_THRESHOLD],
inset: [VIEW_TRIGGER_INSET],
},
effects: [EFFECT_DEFINITIONS],
// and/or
sequences: [SEQUENCE_DEFINITIONS],
},
],
};
const css = generate(config);
```
**Append to `` or beginning of ``:**
```html
```
### Step 2: Mark elements with `initial`
**Web (Custom Elements):**
```html
```
**React:**
```tsx
...
```
**Vanilla:**
```html
```
### Rules
- `generate()` should be called server-side or at build time. Can also be called on the client if the page content is initially hidden (e.g. behind a loader/splash screen).
- `initial` is only valid for `viewEnter` + `triggerType: 'once'` (or no `triggerType`, which defaults to `'once'`) where source and target are the same element.
- Do NOT use `initial` for `viewEnter` with `triggerType: 'repeat'`/`'alternate'`/`'state'`. For those, manually apply the initial keyframe as inline styles on the target element and use `fill: 'both'`.
- `generate(config)` processes all interactions in the config, not just `viewEnter`. Set `initial` only on the relevant `viewEnter` + `triggerType: 'once'` elements.
## Rule 1: keyframeEffect / namedEffect (TimeEffect)
Use `keyframeEffect` or `namedEffect` when the viewEnter should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior. Use `params` only for observer configuration (`threshold`, `inset`).
**Multiple effects:** The `effects` array can contain multiple effects — all share the same viewEnter trigger and fire together when the element enters the viewport. Each effect can have its own `triggerType`. Use this to animate different targets from a single viewport entry event.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'viewEnter',
params: {
threshold: [VISIBILITY_THRESHOLD],
inset: '[VIEWPORT_INSETS]'
},
effects: [
{
key: '[TARGET_KEY]',
selector: '[TARGET_SELECTOR]',
triggerType: '[TRIGGER_TYPE]',
// --- pick ONE of the two effect types ---
keyframeEffect: {
name: '[EFFECT_NAME]',
keyframes: [KEYFRAMES],
},
// OR
namedEffect: [NAMED_EFFECT_DEFINITION],
fill: '[FILL_MODE]',
duration: [DURATION_MS],
easing: '[EASING_FUNCTION]',
delay: [DELAY_MS],
iterations: [ITERATIONS],
alternate: [ALTERNATE_BOOL],
effectId: '[UNIQUE_EFFECT_ID]'
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web/vanilla, `interactKey` for React). The **source element** is observed for viewport intersection. This is the element the IntersectionObserver watches.
- `[TARGET_KEY]` — identifier matching the element's key on the element that animates.
- `[TARGET_SELECTOR]` - optional. Selector for the child element to select inside the root element. For `triggerType` of `'alternate'`/`'repeat'`/`'state'` MUST either use a separate `[TARGET_KEY]` from `[SOURCE_KEY]` or `selector` for selecting a child element as target.
- `[TRIGGER_TYPE]` — `triggerType` on the effect. One of:
- `'once'` (default) — plays once when the source element first enters the viewport and never again. Source and target may be the same element.
- `'repeat'` — restarts the animation every time the source element enters the viewport. Use separate source and target.
- `'alternate'` — plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target.
- `'state'` — resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`). Use separate source and target.
- `[VISIBILITY_THRESHOLD]` — optional. Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. `0.3` = 30%).
- `[VIEWPORT_INSETS]` — optional. String adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it).
- `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.
- `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`.
- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.
- `[FILL_MODE]` — `'both'` for `triggerType: 'alternate'`, `'repeat'`, or `'state'`. For `triggerType: 'once'`: use `'backwards'` when the animation's final keyframe has no additional effect (over element's base style); use `'both'` otherwise.
- `[DURATION_MS]` — animation duration in milliseconds.
- `[EASING_FUNCTION]` — CSS easing string or named easing from `@wix/motion`.
- `[DELAY_MS]` — optional delay before the effect starts, in milliseconds.
- `[ITERATIONS]` — optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.
- `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration (within a single playback).
- `[UNIQUE_EFFECT_ID]` — optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects.
---
## Rule 2: customEffect (TimeEffect)
Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'viewEnter',
params: {
threshold: [VISIBILITY_THRESHOLD],
inset: '[VIEWPORT_INSETS]'
},
effects: [
{
key: '[TARGET_KEY]',
triggerType: '[TRIGGER_TYPE]',
customEffect: [CUSTOM_EFFECT_CALLBACK],
duration: [DURATION_MS],
easing: '[EASING_FUNCTION]',
effectId: '[UNIQUE_EFFECT_ID]'
}
]
}
```
### Variables
- `[SOURCE_KEY]` / `[TARGET_KEY]` / `[TRIGGER_TYPE]` / `[VISIBILITY_THRESHOLD]` / `[VIEWPORT_INSETS]` / `[DURATION_MS]` / `[EASING_FUNCTION]` / `[UNIQUE_EFFECT_ID]` — same as Rule 1.
- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with `element` being the target element, and `progress` from 0 to 1.
---
## Rule 3: Sequences
Use sequences when a viewEnter should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior.
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'viewEnter',
params: {
threshold: [VISIBILITY_THRESHOLD],
inset: '[VIEWPORT_INSETS]'
},
sequences: [
{
triggerType: '[TRIGGER_TYPE]',
offset: [OFFSET_MS],
offsetEasing: '[OFFSET_EASING]',
effects: [
[EFFECT_DEFINTION],
// .. more effects as necessary
]
}
]
}
```
### Variables
- `[SOURCE_KEY]` / `[VISIBILITY_THRESHOLD]` / `[VIEWPORT_INSETS]` — same as Rule 1.
- `[TRIGGER_TYPE]` — same as Rule 1. `triggerType` is set on the sequence config, not on individual effects within the sequence.
- `[OFFSET_MS]` — time offset between each child's animation start, in milliseconds.
- `[OFFSET_EASING]` — CSS easing or named easing from `@wix/motion`, for the stagger distribution. Defaults to `'linear'`.
- `[EFFECT_DEFINTION]` — a definition of or a reference to a time-based animation effect.
--- viewprogress.md ---
# ViewProgress Trigger Rules for @wix/interact
These rules help generate scroll-driven interactions using `@wix/interact`. ViewProgress triggers create animations that update continuously as elements move through the viewport, leveraging native CSS ViewTimelines where supported, and using a polyfill library where unsupported. Use when animation progress should be tied to the element's scroll position.
> **CRITICAL:** You MUST replace all usage of `overflow: hidden` with `overflow: clip` on every element between the trigger source element and the scroll container. `overflow: hidden` creates a new scroll context that breaks the ViewTimeline; `overflow: clip` clips overflow visually without affecting scroll ancestry. If using Tailwind, replace all `overflow-hidden` classes with `overflow-clip`.
**Offset semantics:** The `offset` inside `rangeStart`/`rangeEnd` is an object `{ unit: 'percentage', value: NUMBER }` where value is 0–100. For absolute lengths use `{ unit: 'px', value: NUMBER }` (or other CSS length units). Positive values move the effective range boundary forward along the scroll axis.
## 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)
- [Pre-rendering Scroll-driven CSS with generate()](#pre-rendering-scroll-driven-css-with-generate)
---
## Rule 1: ViewProgress with keyframeEffect or namedEffect
**Use Case**: Scroll-driven CSS-based effects.
**Multiple effects:** The `effects` array can contain multiple effects — all are driven by the same scroll progress. Use this to animate different targets or properties in sync with the same scroll position.
### Template
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'viewProgress',
effects: [
{
key: '[TARGET_KEY]',
// --- pick ONE of the two effect types ---
namedEffect: [NAMED_EFFECT_DEFINITION],
// 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]', // usually 'linear'
fill: 'both',
effectId: '[UNIQUE_EFFECT_ID]'
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). The element whose scroll position drives the animation.
- `[TARGET_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React) on the element to animate (can be same as source or different).
- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. **CRITICAL:** Scroll presets (`*Scroll`) MUST include `range: 'in' | 'out' | 'continuous'` in their options. `'in'` ends at the idle state, `'out'` starts from the idle state, `'continuous'` passes through it.
- `[EFFECT_NAME]` — unique name for custom keyframe effect.
- `[EFFECT_KEYFRAMES]` — array of keyframe objects defining CSS property values (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.
- `[RANGE_NAME]` — scroll range name:
- `'cover'` — full visibility span from first pixel entering to last pixel leaving.
- `'entry'` — the phase while the element is entering the viewport.
- `'exit'` — the phase while the element is exiting the viewport.
- `'contain'` — while the element is fully contained in the viewport. Typically used with a `position: sticky` container.
- `'entry-crossing'` — from the element's leading edge entering to its leading edge reaching the opposite side.
- `'exit-crossing'` — from the element's trailing edge reaching the start to its trailing edge leaving.
- `[START_PERCENTAGE]` — 0–100, starting point within the named range.
- `[END_PERCENTAGE]` — 0–100, end point within the named range.
- `[EASING_FUNCTION]` - CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects.
- `[UNIQUE_EFFECT_ID]` — optional identifier for referencing the effect externally.
---
## Rule 2: ViewProgress with customEffect
**Use Case**: Scroll-driven effects requiring JavaScript logic (e.g., changing SVG attributes, controlling WebGL/WebGPU effects).
### Template
```typescript
{
key: '[SOURCE_KEY]',
trigger: 'viewProgress',
effects: [
{
key: '[TARGET_KEY]',
customEffect: [CUSTOM_EFFECT_CALLBACK],
rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } },
rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } },
easing: `'[EASING_FUNCTION]'`, // usually 'linear'
fill: 'both',
effectId: '[UNIQUE_EFFECT_ID]'
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[SOURCE_KEY]` / `[TARGET_KEY]` — 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.
- `[RANGE_NAME]` / `[START_PERCENTAGE]` / `[END_PERCENTAGE]` — same as Rule 1.
- `[EASING_FUNCTION]` — CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects.
- `[UNIQUE_EFFECT_ID]` — optional identifier for referencing the effect externally.
---
## Rule 3: ViewProgress with Tall Wrapper + Sticky Container (contain range)
**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** (`[TALL_WRAPPER_KEY]`): An element with enough height to create scroll distance (e.g., `height: 300vh`). This is the ViewTimeline source. The taller it is relative to the viewport, the longer the scroll distance and the more "duration" the animation has.
- **Sticky container**: A direct child with `position: sticky; top: 0; height: 100vh` that stays fixed in the viewport while the wrapper scrolls past.
- **Animated elements** (`[STICKY_CHILD_KEY]`): Children of the sticky container that receive the effects.
### Template
```typescript
{
key: '[TALL_WRAPPER_KEY]',
trigger: 'viewProgress',
effects: [
{
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]', // usually 'linear'
fill: 'both',
effectId: '[UNIQUE_EFFECT_ID]'
},
// additional effects targeting other elements can be added here
]
}
```
### Variables
- `[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]` — 0–100, starting point within the `contain` range (the stuck phase).
- `[END_PERCENTAGE]` — 0–100, end point within the `contain` range.
- `[UNIQUE_EFFECT_ID]` — same as Rule 1.
- `[EASING_FUNCTION]` — CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects.
---
## Pre-rendering Scroll-driven CSS with generate()
Call `generate(config)` server-side or at build time to produce native scroll-driven CSS for `viewProgress` interactions. The generated output includes `view-timeline` declarations, `animation-timeline`/`animation-range` custom properties, and `@keyframes` — everything the browser needs to run scroll-driven animations without any JavaScript.
```typescript
import { generate } from '@wix/interact';
const config = {
interactions: [
{
key: 'parallax-hero',
trigger: 'viewProgress',
effects: [
{
keyframeEffect: {
name: 'parallax',
keyframes: [{ transform: 'translateY(50px)' }, { transform: 'translateY(-50px)' }],
},
rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 0 } },
rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 100 } },
fill: 'both',
},
],
},
],
effects: {},
};
const css = generate(config, false);
```
Inject the resulting CSS into `` so scroll-driven animations are ready before JS loads:
```html
```
**Benefits:**
- **Zero JS scroll overhead.** The browser drives animations based on scroll position natively via CSS `view-timeline` — no JS scroll listeners, no `requestAnimationFrame` polling.
- **No DOM references needed.** CSS attribute selectors (`[data-interact-key]`) bind to elements reactively as they appear in the DOM. No `querySelector`, no cached references, no lifecycle management.
- **Instant first paint.** Animations work as soon as CSS is parsed, before JS loads or hydrates.
No `initial` attribute is needed for scroll-driven animations — unlike `viewEnter` FOUC prevention, there is no flash-of-content concern since the animation is continuously driven by scroll position.
> **Note:** `generate()` processes all interactions in the config, not just `viewProgress`. If your config also includes `viewEnter`, `hover`, `click`, or other triggers, CSS for those is generated too.