@wix/interact 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/cjs/index.js +1 -1
  2. package/dist/cjs/react.js +1 -1
  3. package/dist/cjs/web.js +1 -1
  4. package/dist/cjs/web.js.map +1 -1
  5. package/dist/es/index.js +1 -1
  6. package/dist/es/react.js +2 -2
  7. package/dist/es/web.js +15 -15
  8. package/dist/es/web.js.map +1 -1
  9. package/dist/index-ByLXasWO.mjs +2832 -0
  10. package/dist/index-ByLXasWO.mjs.map +1 -0
  11. package/dist/index-CzRuJxn8.js +18 -0
  12. package/dist/index-CzRuJxn8.js.map +1 -0
  13. package/dist/tsconfig.build.tsbuildinfo +1 -1
  14. package/dist/types/core/Interact.d.ts +2 -2
  15. package/dist/types/core/InteractionController.d.ts +2 -2
  16. package/dist/types/core/InteractionController.d.ts.map +1 -1
  17. package/dist/types/core/add.d.ts.map +1 -1
  18. package/dist/types/core/css.d.ts.map +1 -1
  19. package/dist/types/handlers/effectHandlers.d.ts +4 -4
  20. package/dist/types/handlers/effectHandlers.d.ts.map +1 -1
  21. package/dist/types/handlers/eventTrigger.d.ts +2 -2
  22. package/dist/types/handlers/eventTrigger.d.ts.map +1 -1
  23. package/dist/types/handlers/index.d.ts.map +1 -1
  24. package/dist/types/handlers/viewEnter.d.ts.map +1 -1
  25. package/dist/types/index.d.ts +1 -1
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/types/react/index.d.ts +1 -1
  28. package/dist/types/react/index.d.ts.map +1 -1
  29. package/dist/types/types/config.d.ts +47 -0
  30. package/dist/types/types/config.d.ts.map +1 -0
  31. package/dist/types/types/controller.d.ts +34 -0
  32. package/dist/types/types/controller.d.ts.map +1 -0
  33. package/dist/types/types/effects.d.ts +75 -0
  34. package/dist/types/types/effects.d.ts.map +1 -0
  35. package/dist/types/types/external.d.ts +6 -0
  36. package/dist/types/types/external.d.ts.map +1 -0
  37. package/dist/types/types/global.d.ts +11 -0
  38. package/dist/types/types/global.d.ts.map +1 -0
  39. package/dist/types/types/handlers.d.ts +41 -0
  40. package/dist/types/types/handlers.d.ts.map +1 -0
  41. package/dist/types/types/index.d.ts +8 -0
  42. package/dist/types/types/index.d.ts.map +1 -0
  43. package/dist/types/types/internal.d.ts +36 -0
  44. package/dist/types/types/internal.d.ts.map +1 -0
  45. package/dist/types/types/triggers.d.ts +28 -0
  46. package/dist/types/types/triggers.d.ts.map +1 -0
  47. package/dist/types/web/InteractElement.d.ts +2 -2
  48. package/dist/types/web/InteractElement.d.ts.map +1 -1
  49. package/dist/types/web/index.d.ts +1 -1
  50. package/dist/types/web/index.d.ts.map +1 -1
  51. package/docs/api/README.md +2 -3
  52. package/docs/api/functions.md +4 -4
  53. package/docs/api/interact-class.md +2 -3
  54. package/docs/api/interact-element.md +2 -2
  55. package/docs/api/interaction-controller.md +4 -4
  56. package/docs/api/types.md +38 -69
  57. package/docs/examples/README.md +1 -1
  58. package/docs/examples/click-interactions.md +0 -7
  59. package/docs/examples/entrance-animations.md +28 -27
  60. package/docs/examples/list-patterns.md +17 -16
  61. package/docs/guides/conditions-and-media-queries.md +2 -3
  62. package/docs/guides/configuration-structure.md +5 -7
  63. package/docs/guides/effects-and-animations.md +2 -4
  64. package/docs/guides/getting-started.md +0 -1
  65. package/docs/guides/lists-and-dynamic-content.md +10 -9
  66. package/docs/guides/sequences.md +3 -4
  67. package/docs/guides/state-management.md +0 -2
  68. package/docs/guides/understanding-triggers.md +9 -13
  69. package/package.json +2 -2
  70. package/rules/click.md +96 -560
  71. package/rules/full-lean.md +536 -360
  72. package/rules/hover.md +107 -530
  73. package/rules/integration.md +212 -261
  74. package/rules/pointermove.md +154 -1407
  75. package/rules/viewenter.md +128 -863
  76. package/rules/viewprogress.md +88 -322
  77. package/dist/index-BtEG0cjF.mjs +0 -2791
  78. package/dist/index-BtEG0cjF.mjs.map +0 -1
  79. package/dist/index-ErMKtmX2.js +0 -18
  80. package/dist/index-ErMKtmX2.js.map +0 -1
  81. package/dist/types/types.d.ts +0 -256
  82. package/dist/types/types.d.ts.map +0 -1
  83. package/rules/MASTER-CLEANUP-PLAN.md +0 -286
  84. package/rules/scroll-list.md +0 -748
@@ -1,959 +1,224 @@
1
1
  # ViewEnter Trigger Rules for @wix/interact
2
2
 
3
- 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.
3
+ 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.
4
4
 
5
- ## Rule 1: ViewEnter with Once Type for Entrance Animations
6
-
7
- **Use Case**: One-time entrance animations that play when elements first become visible (e.g., hero sections, content blocks, images, cards)
8
-
9
- **When to Apply**:
10
-
11
- - For entrance animations that should only happen once
12
- - When you want elements to stay in their final animated state
13
- - For progressive content reveal as user scrolls
14
- - When implementing lazy-loading visual effects
15
-
16
- **Pattern**:
17
-
18
- ```typescript
19
- {
20
- key: '[SOURCE_KEY]',
21
- trigger: 'viewEnter',
22
- params: {
23
- type: 'once',
24
- threshold: [VISIBILITY_THRESHOLD],
25
- inset: '[VIEWPORT_INSETS]'
26
- },
27
- effects: [
28
- {
29
- key: '[TARGET_KEY]',
30
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
31
- duration: [DURATION_MS],
32
- easing: '[EASING_FUNCTION]',
33
- delay: [DELAY_MS],
34
- effectId: '[UNIQUE_EFFECT_ID]'
35
- }
36
- ]
37
- }
38
- ```
39
-
40
- **Variables**:
41
-
42
- - `[SOURCE_KEY]`: Unique identifier for element that triggers when visible (often same as target key)
43
- - `[TARGET_KEY]`: Unique identifier for element to animate (can be same as source or different)
44
- - `[VISIBILITY_THRESHOLD]`: Number between 0-1 indicating how much of element must be visible (e.g., 0.3 = 30%)
45
- - `[VIEWPORT_INSETS]`: String insets around viewport (e.g., '50px', '10%', '-100px')
46
- - `[EFFECT_TYPE]`: Either `namedEffect` or `keyframeEffect`
47
- - `[EFFECT_DEFINITION]`: Named effect string (e.g., 'FadeIn', 'SlideIn') or keyframe object
48
- - `[DURATION_MS]`: Animation duration in milliseconds (typically 500-1200ms for entrances)
49
- - `[EASING_FUNCTION]`: Timing function (recommended: 'ease-out', 'cubic-bezier(0.16, 1, 0.3, 1)')
50
- - `[DELAY_MS]`: Optional delay before animation starts
51
- - `[UNIQUE_EFFECT_ID]`: Optional unique identifier for animation chaining
5
+ ---
52
6
 
53
- **Example - Hero Section Entrance**:
7
+ > **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.
54
8
 
55
- ```typescript
56
- {
57
- key: 'hero-section',
58
- trigger: 'viewEnter',
59
- params: {
60
- type: 'once',
61
- threshold: 0.3,
62
- inset: '-100px'
63
- },
64
- effects: [
65
- {
66
- key: 'hero-section',
67
- keyframeEffect: {
68
- name: 'hero-entrance',
69
- keyframes: [
70
- { opacity: '0', transform: 'translateY(60px) scale(0.95)' },
71
- { opacity: '1', transform: 'translateY(0) scale(1)' }
72
- ]
73
- },
74
- duration: 1000,
75
- easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
76
- fill: 'backwards',
77
- effectId: 'hero-entrance'
78
- }
79
- ]
80
- }
81
- ```
9
+ ## Table of Contents
82
10
 
83
- **Example - Content Block Fade In**:
84
-
85
- ```typescript
86
- {
87
- key: 'content-block',
88
- trigger: 'viewEnter',
89
- params: {
90
- type: 'once',
91
- threshold: 0.5
92
- },
93
- effects: [
94
- {
95
- key: 'content-block',
96
- namedEffect: {
97
- type: 'FadeIn'
98
- },
99
- duration: 800,
100
- easing: 'ease-out',
101
- fill: 'backwards'
102
- }
103
- ]
104
- }
105
- ```
11
+ - [Preventing Flash of Unstyled Content (FOUC)](#preventing-flash-of-unstyled-content-fouc)
12
+ - [Rule 1: keyframeEffect / namedEffect (TimeEffect)](#rule-1-keyframeeffect--namedeffect-timeeffect)
13
+ - [Rule 2: customEffect (TimeEffect)](#rule-2-customeffect-timeeffect)
14
+ - [Rule 3: Sequences](#rule-3-sequences)
106
15
 
107
16
  ---
108
17
 
109
- ## Rule 2: ViewEnter with Repeat Type and Separate Source/Target
110
-
111
- **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)
112
-
113
- **When to Apply**:
18
+ ## Preventing Flash of Unstyled Content (FOUC)
114
19
 
115
- - When animations should replay on each scroll encounter
116
- - For scroll-triggered interactive elements
117
- - When using separate observer and animation targets
118
- - For elements that might leave and re-enter viewport
20
+ **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.
119
21
 
120
- **Pattern**:
22
+ **Solution:** Two things are required — **both** MUST be present for FOUC prevention to work:
121
23
 
122
- ```typescript
123
- {
124
- key: '[OBSERVER_KEY]',
125
- trigger: 'viewEnter',
126
- params: {
127
- type: 'repeat',
128
- threshold: [VISIBILITY_THRESHOLD],
129
- inset: '[VIEWPORT_INSETS]'
130
- },
131
- effects: [
132
- {
133
- key: '[ANIMATION_TARGET_KEY]',
134
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
135
- duration: [DURATION_MS],
136
- easing: '[EASING_FUNCTION]',
137
- delay: [DELAY_MS],
138
- effectId: '[UNIQUE_EFFECT_ID]'
139
- }
140
- ]
141
- }
142
- ```
24
+ 1. **Generate critical CSS** using `generate(config)` — produces CSS rules that hide entrance-animated elements from the moment the page renders, before JavaScript runs.
25
+ 2. **Mark elements with `initial`** — set `data-interact-initial="true"` on `<interact-element>`, or `initial={true}` on the `<Interaction>` React component. This tells the runtime which elements have critical CSS applied.
143
26
 
144
- **Variables**:
27
+ If only one of these is present, FOUC prevention will **not** work. Both the CSS and the `initial` attribute are required.
145
28
 
146
- - `[OBSERVER_KEY]`: Unique identifier for element that acts as scroll trigger
147
- - `[ANIMATION_TARGET_KEY]`: Unique identifier for element that gets animated (different from observer)
148
- - Other variables same as Rule 1
29
+ ### Step 1: Generate CSS and inject into `<head>` (preferred), or beginning of `<body>`
149
30
 
150
- **Example - Image Reveal on Scroll**:
31
+ Call `generate(config)` server-side or at build time. Inject the resulting CSS into the document `<head>` (or in `<body>` before your content) so it loads before the page content is painted:
151
32
 
152
33
  ```typescript
153
- {
154
- key: 'image-trigger-zone',
155
- trigger: 'viewEnter',
156
- params: {
157
- type: 'repeat',
158
- threshold: 0.1,
159
- inset: '-50px'
160
- },
161
- effects: [
162
- {
163
- key: 'background-image',
164
- keyframeEffect: {
165
- name: 'image-reveal',
166
- keyframes: [
167
- { filter: 'blur(20px) brightness(0.7)', transform: 'scale(1.1)' },
168
- { filter: 'blur(0) brightness(1)', transform: 'scale(1)' }
169
- ]
170
- },
171
- duration: 600,
172
- easing: 'ease-out',
173
- fill: 'backwards'
174
- }
175
- ]
176
- }
177
- ```
34
+ import { generate } from '@wix/interact';
178
35
 
179
- **Example - Counter Animation Repeat**:
180
-
181
- ```typescript
182
- {
183
- key: 'stats-section',
184
- trigger: 'viewEnter',
185
- params: {
186
- type: 'repeat',
187
- threshold: 0.6
36
+ const config: InteractConfig = {
37
+ interactions: [
38
+ {
39
+ key: '[SOURCE_KEY]',
40
+ trigger: 'viewEnter',
41
+ params: {
42
+ threshold: [VIEW_TRIGGER_THRESHOLD],
43
+ inset: [VIEW_TRIGGER_INSET],
44
+ },
45
+ effects: [EFFECT_DEFINITIONS],
46
+ // and/or
47
+ sequences: [SEQUENCE_DEFINITIONS],
188
48
  },
189
- effects: [
190
- {
191
- key: 'counter-display',
192
- customEffect: (element, progress) => {
193
- const targetValue = 1000;
194
- const currentValue = Math.floor(targetValue * progress);
195
- element.textContent = currentValue.toLocaleString();
196
- },
197
- duration: 2000,
198
- easing: 'ease-out',
199
- effectId: 'counter-animation'
200
- }
201
- ]
202
- }
203
- ```
204
-
205
- ---
49
+ ],
50
+ };
206
51
 
207
- ## Rule 3: ViewEnter with Alternate Type and Separate Source/Target
52
+ const css = generate(config);
53
+ ```
208
54
 
209
- **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)
55
+ **Append to `<head>` or beginning of `<body>`:**
210
56
 
211
- **When to Apply**:
57
+ ```html
58
+ <style>
59
+ ${css}
60
+ </style>
61
+ ```
212
62
 
213
- - For animations that should reverse when element exits viewport
214
- - When creating scroll-responsive reveals
215
- - For elements that animate in and out smoothly
216
- - When observer element is different from animated element
63
+ ### Step 2: Mark elements with `initial`
217
64
 
218
- **Pattern**:
65
+ **Web (Custom Elements):**
219
66
 
220
- ```typescript
221
- {
222
- key: '[OBSERVER_KEY]',
223
- trigger: 'viewEnter',
224
- params: {
225
- type: 'alternate',
226
- threshold: [VISIBILITY_THRESHOLD],
227
- inset: '[VIEWPORT_INSETS]'
228
- },
229
- effects: [
230
- {
231
- key: '[ANIMATION_TARGET_KEY]',
232
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
233
- duration: [DURATION_MS],
234
- easing: '[EASING_FUNCTION]',
235
- effectId: '[UNIQUE_EFFECT_ID]'
236
- }
237
- ]
238
- }
67
+ ```html
68
+ <interact-element data-interact-key="[SOURCE_KEY]" data-interact-initial="true">
69
+ <section>...</section>
70
+ </interact-element>
239
71
  ```
240
72
 
241
- **Variables**:
242
- Same as Rule 2
73
+ **React:**
243
74
 
244
- **Example - Content Reveal with Hide**:
245
-
246
- ```typescript
247
- {
248
- key: 'content-trigger',
249
- trigger: 'viewEnter',
250
- params: {
251
- type: 'alternate',
252
- threshold: 0.3,
253
- inset: '-20px'
254
- },
255
- effects: [
256
- {
257
- key: 'sidebar-content',
258
- keyframeEffect: {
259
- name: 'content-reveal-hide',
260
- keyframes: [
261
- { opacity: '0', transform: 'translateX(-50px)' },
262
- { opacity: '1', transform: 'translateX(0)' }
263
- ]
264
- },
265
- duration: 400,
266
- easing: 'ease-in-out',
267
- fill: 'backwards'
268
- }
269
- ]
270
- }
75
+ ```tsx
76
+ <Interaction tagName="section" interactKey="[SOURCE_KEY]" initial={true}>
77
+ ...
78
+ </Interaction>
271
79
  ```
272
80
 
273
- **Example - Navigation Bar Reveal**:
81
+ **Vanilla:**
274
82
 
275
- ```typescript
276
- {
277
- key: 'page-content',
278
- trigger: 'viewEnter',
279
- params: {
280
- type: 'alternate',
281
- threshold: 0.1
282
- },
283
- effects: [
284
- {
285
- key: 'floating-nav',
286
- keyframeEffect: {
287
- name: 'nav-reveal',
288
- keyframes: [
289
- { opacity: '0', transform: 'translateY(-100%)' },
290
- { opacity: '1', transform: 'translateY(0)' }
291
- ]
292
- },
293
- duration: 300,
294
- easing: 'ease-out',
295
- fill: 'backwards',
296
- effectId: 'nav-reveal'
297
- }
298
- ]
299
- }
83
+ ```html
84
+ <section data-interact-key="[SOURCE_KEY]" data-interact-initial="true">...</section>
300
85
  ```
301
86
 
302
- ---
303
-
304
- ## Rule 4: ViewEnter with State Type for Loop Animations
87
+ ### Rules
305
88
 
306
- **Use Case**: Looping animations that start when element enters viewport and can be paused/resumed (e.g., ambient animations, loading states, decorative effects)
89
+ - `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).
90
+ - `initial` is only valid for `viewEnter` + `triggerType: 'once'` (or no `triggerType`, which defaults to `'once'`) where source and target are the same element.
91
+ - 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'`.
92
+ - If other interactions in the config also need FOUC prevention, `generate(config)` covers them all — set `initial` only on the relevant `viewEnter` + `triggerType: 'once'` elements.
307
93
 
308
- **When to Apply**:
94
+ ## Rule 1: keyframeEffect / namedEffect (TimeEffect)
309
95
 
310
- - For continuous animations that should start on viewport enter
311
- - When you need pause/resume control over scroll-triggered loops
312
- - For ambient or decorative animations
313
- - When creating scroll-activated background effects
96
+ 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`).
314
97
 
315
- **Pattern**:
98
+ **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.
316
99
 
317
100
  ```typescript
318
101
  {
319
102
  key: '[SOURCE_KEY]',
320
103
  trigger: 'viewEnter',
321
104
  params: {
322
- type: 'state',
323
105
  threshold: [VISIBILITY_THRESHOLD],
324
106
  inset: '[VIEWPORT_INSETS]'
325
107
  },
326
108
  effects: [
327
109
  {
328
110
  key: '[TARGET_KEY]',
329
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
330
- duration: [DURATION_MS],
331
- easing: '[EASING_FUNCTION]',
332
- iterations: [ITERATION_COUNT],
333
- alternate: [ALTERNATE_BOOLEAN],
334
- effectId: '[UNIQUE_EFFECT_ID]'
335
- }
336
- ]
337
- }
338
- ```
339
-
340
- **Variables**:
111
+ selector: '[TARGET_SELECTOR]',
112
+ triggerType: '[TRIGGER_TYPE]',
341
113
 
342
- - `[ITERATION_COUNT]`: Number of iterations or Infinity for continuous looping
343
- - `[ALTERNATE_BOOLEAN]`: true/false - whether to reverse on alternate iterations
344
- - Other variables same as Rule 1
345
-
346
- **Example - Floating Animation Loop**:
347
-
348
- ```typescript
349
- {
350
- key: 'floating-elements',
351
- trigger: 'viewEnter',
352
- params: {
353
- type: 'state',
354
- threshold: 0.4
355
- },
356
- effects: [
357
- {
358
- key: 'floating-icon',
114
+ // --- pick ONE of the two effect types ---
359
115
  keyframeEffect: {
360
- name: 'floating-loop',
361
- keyframes: [
362
- { transform: 'translateY(0)' },
363
- { transform: 'translateY(-20px)' },
364
- { transform: 'translateY(0)' }
365
- ]
116
+ name: '[EFFECT_NAME]',
117
+ keyframes: [KEYFRAMES],
366
118
  },
367
- duration: 3000,
368
- easing: 'ease-in-out',
369
- iterations: Infinity,
370
- alternate: false,
371
- effectId: 'floating-loop'
372
- }
373
- ]
374
- }
375
- ```
119
+ // OR
120
+ namedEffect: [NAMED_EFFECT_DEFINITION],
376
121
 
377
- **Example - Breathing Light Effect**:
378
-
379
- ```typescript
380
- {
381
- key: 'ambient-section',
382
- trigger: 'viewEnter',
383
- params: {
384
- type: 'state',
385
- threshold: 0.2
386
- },
387
- effects: [
388
- {
389
- key: 'light-orb',
390
- namedEffect: {
391
- type: 'Pulse'
392
- },
393
- duration: 2000,
394
- easing: 'ease-in-out',
395
- iterations: Infinity,
396
- alternate: true,
397
- effectId: 'breathing-light'
398
- }
399
- ]
400
- }
401
- ```
122
+ fill: '[FILL_MODE]',
123
+ duration: [DURATION_MS],
124
+ easing: '[EASING_FUNCTION]',
125
+ delay: [DELAY_MS],
126
+ iterations: [ITERATIONS],
127
+ alternate: [ALTERNATE_BOOL],
128
+ effectId: '[UNIQUE_EFFECT_ID]'
129
+ },
130
+ // additional effects targeting other elements can be added here
131
+ ]
132
+ }
133
+ ```
134
+
135
+ ### Variables
136
+
137
+ - `[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.
138
+ - `[TARGET_KEY]` — identifier matching the element's key on the element that animates.
139
+ - `[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.
140
+ - `[TRIGGER_TYPE]` — `triggerType` on the effect. One of:
141
+ - `'once'` (default) — plays once when the source element first enters the viewport and never again. Source and target may be the same element.
142
+ - `'repeat'` — restarts the animation every time the source element enters the viewport. Use separate source and target.
143
+ - `'alternate'` — plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target.
144
+ - `'state'` — resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`). Use separate source and target.
145
+ - `[VISIBILITY_THRESHOLD]` — optional. Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. `0.3` = 30%).
146
+ - `[VIEWPORT_INSETS]` — optional. String adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it).
147
+ - `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.
148
+ - `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`.
149
+ - `[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.
150
+ - `[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.
151
+ - `[DURATION_MS]` — animation duration in milliseconds.
152
+ - `[EASING_FUNCTION]` — CSS easing string or named easing from `@wix/motion`.
153
+ - `[DELAY_MS]` — optional delay before the effect starts, in milliseconds.
154
+ - `[ITERATIONS]` — optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.
155
+ - `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration (within a single playback).
156
+ - `[UNIQUE_EFFECT_ID]` — optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects.
402
157
 
403
158
  ---
404
159
 
405
- ## Rule 5: Threshold and Viewport Intersection Parameters
406
-
407
- **Use Case**: Fine-tuning when animations trigger based on element visibility and viewport positioning (e.g., early triggers, late triggers, precise timing)
160
+ ## Rule 2: customEffect (TimeEffect)
408
161
 
409
- **When to Apply**:
410
-
411
- - When default triggering timing isn't optimal
412
- - For elements that need early or late animation triggers
413
- - When working with very tall or very short elements
414
- - For precise scroll timing control
415
-
416
- **Pattern**:
162
+ 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.
417
163
 
418
164
  ```typescript
419
165
  {
420
166
  key: '[SOURCE_KEY]',
421
167
  trigger: 'viewEnter',
422
168
  params: {
423
- type: '[BEHAVIOR_TYPE]',
424
- threshold: [PRECISE_THRESHOLD],
425
- inset: '[VIEWPORT_ADJUSTMENT]'
169
+ threshold: [VISIBILITY_THRESHOLD],
170
+ inset: '[VIEWPORT_INSETS]'
426
171
  },
427
172
  effects: [
428
173
  {
429
174
  key: '[TARGET_KEY]',
430
- [EFFECT_TYPE]: [EFFECT_DEFINITION],
175
+ triggerType: '[TRIGGER_TYPE]',
176
+ customEffect: [CUSTOM_EFFECT_CALLBACK],
431
177
  duration: [DURATION_MS],
432
- easing: '[EASING_FUNCTION]'
433
- }
434
- ]
435
- }
436
- ```
437
-
438
- **Variables**:
439
-
440
- - `[PRECISE_THRESHOLD]`: Decimal between 0-1 for exact visibility percentage
441
- - `[VIEWPORT_ADJUSTMENT]`: Pixel or percentage adjustment to viewport detection area
442
- - `[BEHAVIOR_TYPE]`: 'once', 'repeat', 'alternate', or 'state'
443
- - Other variables same as Rule 1
444
-
445
- **Example - Early Trigger for Tall Elements**:
446
-
447
- ```typescript
448
- {
449
- key: 'tall-hero-section',
450
- trigger: 'viewEnter',
451
- params: {
452
- type: 'once',
453
- threshold: 0.1, // Trigger when only 10% visible
454
- inset: '-200px' // Extend detection area 200px beyond viewport
455
- },
456
- effects: [
457
- {
458
- key: 'tall-hero-section',
459
- namedEffect: {
460
- type: 'SlideIn'
461
- },
462
- duration: 1200,
463
- easing: 'cubic-bezier(0.16, 1, 0.3, 1)'
464
- }
465
- ]
466
- }
467
- ```
468
-
469
- **Example - Late Trigger for Precise Timing**:
470
-
471
- ```typescript
472
- {
473
- key: 'precision-content',
474
- trigger: 'viewEnter',
475
- params: {
476
- type: 'once',
477
- threshold: 0.8, // Wait until 80% visible
478
- inset: '50px' // Shrink detection area by 50px
479
- },
480
- effects: [
481
- {
482
- key: 'precision-content',
483
- keyframeEffect: {
484
- name: 'blur',
485
- keyframes: [
486
- { opacity: '0', filter: 'blur(5px)' },
487
- { opacity: '1', filter: 'blur(0)' }
488
- ]
489
- },
490
- duration: 600,
491
- easing: 'ease-out',
492
- fill: 'backwards'
178
+ easing: '[EASING_FUNCTION]',
179
+ effectId: '[UNIQUE_EFFECT_ID]'
493
180
  }
494
181
  ]
495
182
  }
496
183
  ```
497
184
 
498
- **Example - Mobile vs Desktop Thresholds**:
185
+ ### Variables
499
186
 
500
- ```typescript
501
- {
502
- conditions: {
503
- // Condition IDs are user-defined strings matched against these media predicates
504
- 'desktop-only': { type: 'media', predicate: '(min-width: 768px)' },
505
- },
506
- interactions: [
507
- {
508
- key: 'responsive-element',
509
- trigger: 'viewEnter',
510
- params: {
511
- type: 'once',
512
- threshold: 0.3,
513
- inset: '-100px'
514
- },
515
- conditions: ['desktop-only'],
516
- effects: [
517
- {
518
- key: 'responsive-element',
519
- namedEffect: { type: 'FadeIn' },
520
- duration: 800
521
- }
522
- ]
523
- }
524
- ]
525
- }
526
- ```
187
+ - `[SOURCE_KEY]` / `[TARGET_KEY]` / `[TRIGGER_TYPE]` / `[VISIBILITY_THRESHOLD]` / `[VIEWPORT_INSETS]` / `[DURATION_MS]` / `[EASING_FUNCTION]` / `[UNIQUE_EFFECT_ID]` — same as Rule 1.
188
+ - `[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.
527
189
 
528
190
  ---
529
191
 
530
- ## Rule 6: Staggered Entrance Animations (Sequences)
192
+ ## Rule 3: Sequences
531
193
 
532
- **Use Case**: Sequential entrance animations where multiple elements animate with staggered timing (e.g., card grids, list items, team member cards, feature sections)
533
-
534
- **When to Apply**:
535
-
536
- - When multiple elements should animate in sequence
537
- - For creating wave or cascade effects
538
- - When animating lists, grids, or collections
539
- - For progressive content revelation
540
-
541
- **Preferred approach: use `sequences`** on the interaction instead of manually setting `delay` on individual effects. Sequences automatically calculate stagger delays using `offset` and `offsetEasing`.
542
-
543
- **Pattern (with `listContainer`)**:
194
+ Use sequences when a viewEnter should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior.
544
195
 
545
196
  ```typescript
546
197
  {
547
- key: '[CONTAINER_KEY]',
198
+ key: '[SOURCE_KEY]',
548
199
  trigger: 'viewEnter',
549
200
  params: {
550
- type: '[BEHAVIOR_TYPE]',
551
- threshold: [VISIBILITY_THRESHOLD]
201
+ threshold: [VISIBILITY_THRESHOLD],
202
+ inset: '[VIEWPORT_INSETS]'
552
203
  },
553
204
  sequences: [
554
205
  {
206
+ triggerType: '[TRIGGER_TYPE]',
555
207
  offset: [OFFSET_MS],
556
208
  offsetEasing: '[OFFSET_EASING]',
557
209
  effects: [
558
- {
559
- effectId: '[EFFECT_ID]',
560
- listContainer: '[LIST_CONTAINER_SELECTOR]'
561
- }
210
+ [EFFECT_DEFINTION],
211
+ // .. more effects as necessary
562
212
  ]
563
213
  }
564
214
  ]
565
215
  }
566
216
  ```
567
217
 
568
- **Variables**:
569
-
570
- - `[CONTAINER_KEY]`: Unique identifier for the container element
571
- - `[OFFSET_MS]`: Stagger offset in ms between consecutive items (e.g., 80, 100, 120)
572
- - `[OFFSET_EASING]`: How the offset is distributed — `'linear'` (equal spacing), `'quadIn'` (accelerating), `'sineOut'` (decelerating), etc.
573
- - `[LIST_CONTAINER_SELECTOR]`: CSS selector for the list container whose children become sequence items
574
- - Other variables same as Rule 1
575
-
576
- **Example - Card Grid Stagger (listContainer)**:
577
-
578
- ```typescript
579
- {
580
- key: 'card-grid-container',
581
- trigger: 'viewEnter',
582
- params: {
583
- type: 'once',
584
- threshold: 0.3
585
- },
586
- sequences: [
587
- {
588
- offset: 100,
589
- offsetEasing: 'quadIn',
590
- effects: [
591
- {
592
- effectId: 'card-entrance',
593
- listContainer: '.card-grid'
594
- }
595
- ]
596
- }
597
- ]
598
- }
599
- ```
600
-
601
- With effect in the registry:
602
-
603
- ```typescript
604
- effects: {
605
- 'card-entrance': {
606
- duration: 500,
607
- easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
608
- keyframeEffect: {
609
- name: 'card-fade-up',
610
- keyframes: [
611
- { transform: 'translateY(40px)', opacity: 0 },
612
- { transform: 'translateY(0)', opacity: 1 }
613
- ]
614
- },
615
- fill: 'both'
616
- }
617
- }
618
- ```
619
-
620
- **Example - Feature List Cascade (per-key effects)**:
621
-
622
- When items have individual keys rather than a shared container, list each as a separate effect in the sequence:
623
-
624
- ```typescript
625
- {
626
- key: 'features-section',
627
- trigger: 'viewEnter',
628
- params: {
629
- type: 'once',
630
- threshold: 0.4
631
- },
632
- sequences: [
633
- {
634
- offset: 100,
635
- offsetEasing: 'linear',
636
- effects: [
637
- { effectId: 'feature-slide', key: 'feature-1' },
638
- { effectId: 'feature-slide', key: 'feature-2' },
639
- { effectId: 'feature-slide', key: 'feature-3' }
640
- ]
641
- }
642
- ]
643
- }
644
- ```
645
-
646
- ```typescript
647
- effects: {
648
- 'feature-slide': {
649
- duration: 500,
650
- easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
651
- keyframeEffect: {
652
- name: 'feature-slide-in',
653
- keyframes: [
654
- { opacity: '0', transform: 'translateX(-30px)' },
655
- { opacity: '1', transform: 'translateX(0)' }
656
- ]
657
- },
658
- fill: 'backwards'
659
- }
660
- }
661
- ```
662
-
663
- ---
664
-
665
- ## Advanced Patterns and Combinations
666
-
667
- ### ViewEnter with Animation Chaining
668
-
669
- Using effectId to trigger subsequent animations:
670
-
671
- ```typescript
672
- // Primary entrance
673
- {
674
- key: 'section-container',
675
- trigger: 'viewEnter',
676
- params: {
677
- type: 'once',
678
- threshold: 0.3
679
- },
680
- effects: [
681
- {
682
- key: 'section-title',
683
- namedEffect: {
684
- type: 'FadeIn'
685
- },
686
- duration: 600,
687
- effectId: 'title-entrance'
688
- }
689
- ]
690
- },
691
- // Chained content animation
692
- {
693
- key: 'section-title',
694
- trigger: 'animationEnd',
695
- params: {
696
- effectId: 'title-entrance'
697
- },
698
- effects: [
699
- {
700
- key: 'section-content',
701
- namedEffect: {
702
- type: 'SlideIn'
703
- },
704
- duration: 500,
705
- delay: 100
706
- }
707
- ]
708
- }
709
- ```
710
-
711
- ### Multi-Effect ViewEnter
712
-
713
- Animating multiple targets from single viewport trigger:
714
-
715
- ```typescript
716
- {
717
- key: 'hero-trigger',
718
- trigger: 'viewEnter',
719
- params: {
720
- type: 'once',
721
- threshold: 0.2
722
- },
723
- effects: [
724
- {
725
- key: 'hero-background',
726
- keyframeEffect: {
727
- name: 'blur-bg',
728
- keyframes: [
729
- { filter: 'blur(20px)', transform: 'scale(1.1)' },
730
- { filter: 'blur(0)', transform: 'scale(1)' }
731
- ]
732
- },
733
- duration: 1200,
734
- easing: 'ease-out',
735
- fill: 'backwards'
736
- },
737
- {
738
- key: 'hero-title',
739
- namedEffect: {
740
- type: 'SlideIn'
741
- },
742
- duration: 800,
743
- delay: 300
744
- },
745
- {
746
- key: 'hero-subtitle',
747
- keyframeEffect: {
748
- name: 'subtitle-slide',
749
- keyframes: [
750
- { opacity: '0', transform: 'translateY(30px)' },
751
- { opacity: '1', transform: 'translateY(0)' }
752
- ]
753
- },
754
- duration: 600,
755
- fill: 'backwards',
756
- delay: 600
757
- },
758
- {
759
- key: 'hero-cta',
760
- transition: {
761
- duration: 400,
762
- delay: 900,
763
- styleProperties: [
764
- { name: 'opacity', value: '1' },
765
- { name: 'transform', value: 'translateY(0)' }
766
- ]
767
- }
768
- }
769
- ]
770
- }
771
- ```
772
-
773
- ### Conditional ViewEnter Animations
774
-
775
- 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.
776
-
777
- ```typescript
778
- {
779
- conditions: {
780
- 'desktop-only': { type: 'media', predicate: '(min-width: 768px)' },
781
- 'prefers-motion': { type: 'media', predicate: '(prefers-reduced-motion: no-preference)' },
782
- 'mobile-only': { type: 'media', predicate: '(max-width: 767px)' },
783
- },
784
- interactions: [
785
- {
786
- key: 'responsive-section',
787
- trigger: 'viewEnter',
788
- params: { type: 'once', threshold: 0.5 },
789
- conditions: ['desktop-only', 'prefers-motion'],
790
- effects: [
791
- {
792
- key: 'responsive-section',
793
- namedEffect: { type: 'SlideIn' },
794
- duration: 1000
795
- }
796
- ]
797
- },
798
- // Simplified fallback for mobile or reduced-motion users
799
- {
800
- key: 'responsive-section',
801
- trigger: 'viewEnter',
802
- params: { type: 'once', threshold: 0.7 },
803
- conditions: ['mobile-only'],
804
- effects: [
805
- {
806
- key: 'responsive-section',
807
- namedEffect: { type: 'FadeIn' },
808
- duration: 400
809
- }
810
- ]
811
- }
812
- ]
813
- }
814
- ```
815
-
816
- ---
817
-
818
- ## Preventing Flash of Unstyled Content (FOUC)
819
-
820
- 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.
821
-
822
- **Constraints:**
823
-
824
- - MUST be called server-side or at build time — not client-side
825
- - MUST set `data-interact-initial="true"` on the `<interact-element>` whose first child should be hidden
826
- - Only valid for `viewEnter` + `params.type: 'once'` where source and target are the same element
827
- - Do NOT use for `hover`, `click`, or `viewEnter` with `repeat`/`alternate`/`state` types
828
-
829
- ```typescript
830
- import { generate } from '@wix/interact/web';
831
-
832
- const config: InteractConfig = {
833
- interactions: [
834
- {
835
- key: 'hero',
836
- trigger: 'viewEnter',
837
- params: { type: 'once', threshold: 0.2 },
838
- effects: [{ namedEffect: { type: 'FadeIn' }, duration: 800 }],
839
- },
840
- ],
841
- };
842
-
843
- // Called at build time or on the server
844
- const css = generate(config);
845
-
846
- // Inject into <head> before the page renders
847
- const html = `
848
- <head>
849
- <style>${css}</style>
850
- </head>
851
- <body>
852
- <interact-element data-interact-key="hero" data-interact-initial="true">
853
- <section class="hero">...</section>
854
- </interact-element>
855
- </body>
856
- `;
857
- ```
858
-
859
- ---
860
-
861
- ## Best Practices for ViewEnter Interactions
862
-
863
- ### Behavior Guidelines
864
-
865
- 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
866
-
867
- ### Performance Guidelines
868
-
869
- 1. **Use `once` type for entrance animations** to avoid repeated triggers
870
- 2. **Be careful with separate source/target patterns** - ensure source doesn't get clipped
871
- 3. **Use appropriate thresholds** - avoid triggering too early or too late
872
-
873
- ### Threshold and Timing Guidelines
874
-
875
- 1. **Use realistic thresholds** (0.1-0.5) for natural timing
876
- 2. **Use tiny thresholds for huge elements** (0.01-0.05) for elements much larger than viewport
877
- 3. **Provide adequate inset margins** for mobile viewports
878
- 4. **Keep entrance animations moderate** (500-1200ms)
879
- 5. **Use staggered delays thoughtfully** (50-200ms intervals)
880
-
881
- ### Threshold and Timing Reference
882
-
883
- **Recommended Thresholds by Content Type**:
884
-
885
- - **Hero sections**: 0.1-0.3 (early trigger)
886
- - **Content blocks**: 0.3-0.5 (balanced trigger)
887
- - **Small elements**: 0.5-0.8 (late trigger)
888
- - **Tall sections**: 0.1-0.2 (early trigger)
889
- - **Huge sections**: 0.01-0.05 (ensure trigger)
890
-
891
- **Recommended Insets by Device**:
892
-
893
- - **Desktop**: '-50px' to '-200px'
894
- - **Mobile**: '-20px' to '-100px'
895
- - **Positive insets**: '50px' for precise timing
896
-
897
- ### Common Use Cases by Pattern
898
-
899
- **Once Pattern**:
900
-
901
- - Hero section entrances
902
- - Content block reveals
903
- - Image lazy loading
904
- - Feature introductions
905
- - Call-to-action reveals
906
-
907
- **Repeat Pattern**:
908
-
909
- - Interactive counters
910
- - Scroll-triggered galleries
911
- - Progressive content loading
912
- - Repeated call-to-actions
913
- - Dynamic content sections
914
-
915
- **Alternate Pattern**:
916
-
917
- - Scroll-responsive UI elements
918
- - Reversible content reveals
919
- - Navigation state changes
920
- - Context-sensitive helpers
921
- - Progressive disclosure
922
-
923
- **State Pattern**:
924
-
925
- - Ambient animations
926
- - Background effects
927
- - Decorative elements
928
- - Loading states
929
- - Atmospheric content
930
-
931
- **Staggered Animations**:
932
-
933
- - Card grids and lists
934
- - Team member sections
935
- - Feature comparisons
936
- - Product catalogs
937
- - Timeline elements
938
-
939
- ### Troubleshooting Common Issues
940
-
941
- **ViewEnter not triggering**:
942
-
943
- - Check if source element is clipped by parent overflow
944
- - Verify element exists when `Interact.create()` is called
945
- - Ensure threshold and inset values are appropriate
946
- - Check for conflicting CSS that might hide elements
947
-
948
- **ViewEnter triggering multiple times**:
949
-
950
- - Use `once` type for entrance animations
951
- - Avoid animating the source element if it's also the target
952
- - Consider using separate source and target elements
953
-
954
- **Animation performance issues**:
218
+ ### Variables
955
219
 
956
- - Limit concurrent viewEnter observers
957
- - Use hardware-accelerated properties
958
- - Avoid animating layout properties
959
- - Consider using `will-change` for complex animations
220
+ - `[SOURCE_KEY]` / `[VISIBILITY_THRESHOLD]` / `[VIEWPORT_INSETS]` — same as Rule 1.
221
+ - `[TRIGGER_TYPE]` same as Rule 1. `triggerType` is set on the sequence config, not on individual effects within the sequence.
222
+ - `[OFFSET_MS]` time offset between each child's animation start, in milliseconds.
223
+ - `[OFFSET_EASING]` — CSS easing or named easing from `@wix/motion`, for the stagger distribution. Defaults to `'linear'`.
224
+ - `[EFFECT_DEFINTION]` — a definition of or a reference to a time-based animation effect.