Skip to content

Revisit Interact ruels#159

Open
ydaniv wants to merge 13 commits intomasterfrom
interact-revisit-rules
Open

Revisit Interact ruels#159
ydaniv wants to merge 13 commits intomasterfrom
interact-revisit-rules

Conversation

@ydaniv
Copy link
Collaborator

@ydaniv ydaniv commented Mar 15, 2026

Description

  • Refactor hover.md
  • Refactor click.md
  • Refactor viewprogress.md
  • Refactor viewenter.md
  • Refactor pointermove.md
  • Refactor integration.md
  • Removed scroll-list.md
  • Refactor full-lean.md

Copy link
Contributor

@ameerabuf ameerabuf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the direction, and it does seem to be the same information in much less text, but I do not feel comfortable removing so much without first testing to see there is no regression. We can use Monty's QA-flow on this branch or something like this...

Copy link
Contributor

@ameerabuf ameerabuf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read click and hover. There were already quite a few comments, some are critical IMO, so I submit now and I will continue with the other triggers.

Use `keyframeEffect` or `namedEffect` when the click should play an animation (CSS or WAAPI). Pair with `PointerTriggerParams` to control playback behavior.

**Pattern**:
Always include `fill: 'both'` so the effect remains applied while finished and is not garbage-collected, allowing it to be efficiently toggled.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we can add a keyword like CRITICAL before such statements that will result in breaking the visuals if ignored. Same for the A11y statement at the top.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

trigger: 'click',
params: {
type: 'state'
type: '[POINTER_TYPE]'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the word 'pointer' in context of click/hover can confuse LLMs. Many times I asked for pointer-animations and got hover/click-animations. I believe we should minimize those by trying to avoid using 'pointer' in this context.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed name

{
key: '[TARGET_KEY]',
[EFFECT_TYPE]: [EFFECT_DEFINITION],
fill: '[FILL_MODE]',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if fill is always both in this case there is no reason for the variable

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased

]
}
```
- `[SOURCE_KEY]` — identifier matching the `data-interact-key` attribute on the element that listens for clicks.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need an alternative phrasing here that will also work for React integration

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased

- `'add'` — adds the state on enter.
- `'remove'` — removes the state on enter.
- `'toggle'` — adds the state on enter, removes on leave. Default.
- `'clear'` — clears all previously applied states on enter.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here I don't get this one, is this for overriding on breakpoint?

- Card hover affecting image, text, and button
- Navigation item hover affecting icon and text
- Complex component state changes
Use `transition` when all properties share timing. Use `transitionProperties` when each property needs independent `duration`, `delay`, or `easing`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarities like this between rules of the different triggers make me think that maybe having transition-effect.md, custom-effect.md, etc...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could also help us make sure there are no inconsistencies as there will be one source of truth (imagine editing all the relevant triggers accordingly every time you change slightly the interface of an effect-type)

## Rule 4: Sequences

**Pattern**:
Use sequences when a hover should sync/stagger animations across multiple elements.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mention the shared container parent?

duration: [DURATION_MS],
easing: '[EASING_FUNCTION]',
fill: '[FILL_MODE]',
keyframeEffect: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namedEffect?

- `[SOURCE_KEY]` — same as Rule 1.
- `[POINTER_TYPE]` — same as Rule 1.
- `[OFFSET_MS]` — time offset between each child's animation start, in milliseconds.
- `[OFFSET_EASING]` — easing curve for the stagger distribution (e.g. `'sineOut'`, `'linear'`).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment about the different easing format

# ViewEnter Trigger Rules for @wix/interact

These rules help generate viewport-based interactions using the `@wix/interact` library. ViewEnter triggers use Intersection Observer to detect when elements enter the viewport and are ideal for entrance animations, lazy loading effects, and scroll-triggered content reveals.
This document contains rules for generating viewport-based interactions using the `@wix/interact`. ViewEnter triggers use IntersectionObserver to detect when elements enter the viewport and are ideal for entrance animations, scroll-triggered content reveals, and lazy-loading effects.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

viewport-based is factually correct but confusing. The use of the phrase scroll-triggered is also not ideal and can cause confusion.

- If possible SHOULD be called server-side or at build time - possible also in client, e.g. when entire content is initially hidden
- MUST set `data-interact-initial="true"` on the root element, i.e. with `data-interact-key`, or `<Interaction initial={true}>` for React integration
- Only valid for `viewEnter` + `params.type: 'once'` where source and target are the same element
- Do NOT use for `hover`, `click`, or `viewEnter` with `repeat`/`alternate`/`state` types
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we talking about hover, click here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, what "Do NOT use" mean? generate(config) should still be called for other effects that might need it, but initial should be false. This should be made clear.

- MUST set `data-interact-initial="true"` on the root element, i.e. with `data-interact-key`, or `<Interaction initial={true}>` for React integration
- Only valid for `viewEnter` + `params.type: 'once'` where source and target are the same element
- Do NOT use for `hover`, `click`, or `viewEnter` with `repeat`/`alternate`/`state` types
- For `repeat`/`alternate`/`state` types, MUST manually apply the initial keyframe as style on the target element and use `fill: 'backwards'` or `fill: 'both'` to keep the effect applied when finished, rewound, or reversed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think fill: 'backwards' ever makes sense here, you must use fill: 'both' no?

{
key: 'hero',
trigger: 'viewEnter',
params: { type: 'once', threshold: 0.2 },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep the properties that are not fixed-value templated like in the rules (like threshold, duration, namedEffect), or with placeholder syntax like threshold: <threshold> instead of fixed values

const css = generate(config);

**When to Apply**:
const html = `
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe also a react example?

key: 'feature-card',
key: '[SOURCE_KEY]',
trigger: 'viewProgress',
conditions: ['[CONDITION_NAME]'], // optional
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

irrelevant

{ opacity: '0', transform: 'translateY(-40px)' }
]
key: '[TARGET_KEY]',
customEffect: (element: Element, progress: number) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's at least make this whole rule match the time-based structure of the customEffect rule

Comment on lines +104 to +106
- **Tall wrapper** (`[SOURCE_KEY]`): An element with enough height to create scroll distance (e.g., `height: 300vh`). This is the ViewTimeline source.
- **Sticky container** (`[TARGET_KEY]` or parent of targets): A direct child with `position: sticky; top: 0; height: 100vh` that stays fixed in the viewport while the wrapper scrolls past.
- **Animated elements**: Children of the sticky container that receive the effects.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is considerably less detailed and more limited than the previous version. There are many pitfalls here that make the animation look broken that are not mentioned.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also there are some case where you can get a similar effect using the children as the sources, with easier range calculations


- **Unexpected behavior:** Check range names match intent; verify source visibility; ensure target elements exist.
- **Janky custom effects:** Simplify calculations; avoid layout-triggering reads in the callback.
- `[TALL_WRAPPER_KEY]`: Key for the tall outer element that defines the scroll distance — this is the ViewTimeline source
Copy link
Contributor

@ameerabuf ameerabuf Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that some direction as to how tall it should be will go a long way here.

- `[TALL_WRAPPER_KEY]`: Key for the tall outer element that defines the scroll distance — this is the ViewTimeline source
- `[STICKY_CHILD_KEY]`: Key for the animated element inside the sticky container
- `[EFFECT_NAME]` / `[EFFECT_KEYFRAMES]`: Same as Rule 1
- `[START_PERCENTAGE]` / `[END_PERCENTAGE]`: 0–100 within the `contain` range, i.e. the phase where the sticky container is fully stuck
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe separate to start and end for clarity


### Effect Types for PointerMove
- [Trigger Source Elements with `hitArea: 'self'`](#trigger-source-elements-with-hitarea-self)
- [PointerMoveParams](#pointermoveparams)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you see my point about the confusing naming?

- [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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR but we should consider now having the progress be always a single number and use that axis param also in customEffects


### Hit Area Configuration (`hitArea`)
- The source element **MUST NOT** have `pointer-events: none` — it needs to receive pointer events to track mouse movement.
- **MUST AVOID** using the same element as both source and target with `transform` effects. The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description from hover.md is a bit better IMO, e.g. talking about size and position changes rather than specifically about transform.

const velocity = progress.v || { x: 0, y: 0 };
const speed = Math.sqrt(velocity.x ** 2 + velocity.y ** 2);
- `hitArea` — determines where mouse movement is tracked:
- `'self'` — tracks mouse within the source element's bounds only. Use for local hover effects.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using the word 'hover' might cause confusion.

- `hitArea` — determines where mouse movement is tracked:
- `'self'` — tracks mouse within the source element's bounds only. Use for local hover effects.
- `'root'` — tracks mouse anywhere in the viewport. Use for global cursor followers, ambient effects.
- `axis` — restricts pointer tracking to a single axis. Only relevant when using `keyframeEffect`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it really though? Do we know to ignore it with other types of effects?

## Critical CSS (FOUC Prevention)

### Viewport Entrance
`generate(config)` produces CSS that hides entrance elements until their animation plays. See [viewenter.md](./viewenter.md) for full details.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"entrance elements" is vague

```

### Interactive Toggle (Click)
Inside `<head>`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this more than what is in viewEnter.md that sets the HTML as string to a js const.

| `Interact.create(config)` | Initialize with a config. Returns the instance. |
| `Interact.registerEffects(presets)` | Register named effect presets before `create`. |
| `Interact.destroy()` | Tear down all instances. |
| `Interact.forceReducedMotion` | `boolean` — force reduced-motion behavior regardless of OS setting. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the default?

| `Interact.destroy()` | Tear down all instances. |
| `Interact.forceReducedMotion` | `boolean` — force reduced-motion behavior regardless of OS setting. |
| `Interact.allowA11yTriggers` | `boolean` — enable accessibility triggers. |
| `Interact.setup(options)` | Configure global scroll/pointer/viewEnter options. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does that mean?


---

## Static API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the importance of handling instances is high enough to be included here.

- Create the full configuration up‑front and pass it in a single `create` call to avoid unintended overrides; subsequent calls replace the previous config.

**For web (Custom Elements):**
# @wix/interact — Rules
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upfront I can see that the order and structure of this file completely changed.
The idea of this one was to provide the most lean stand-alone guide for stable usage of LLMs. Meaning if it was the only textual context for the agent, it should be able to get basic working results.
It should go into detail only if necessary to not break, and so it did not review all of the different intricate differences of the triggers.
Also - trouble-shooting and general guidelines were promoted to top after it was usually ignored when at the bottom, and was deemed much more relevant than anything else by the results.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically the idea is less "full documentation" and more "quick guide for dummies"


## Quick Start

Create the full config up-front and pass it in a single `create` call. Subsequent calls create new `Interact` instances.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe best practice of when to intentionally use multiple instances?

const config = {
// config-props
};
Do NOT add observers/listeners manually. The runtime binds triggers and effects via element keys.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be marked as CRITICAL IMO

params?: TriggerParams; // trigger-specific options
effects?: (Effect | EffectRef)[];
sequences?: (SequenceConfig | SequenceConfigRef)[];
conditions?: string[]; // condition ids; all must pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should match to ids from the top-level conditions object

1. **`Effect.key`** — if provided, the target is the `<interact-element>` with matching `data-interact-key`.
2. **Registry Effect's `key`** — if the effect is an `EffectRef`, the `key` from the referenced registry entry is used.
3. **Fallback to `Interaction.key`** — the source element acts as the target.
4. After resolving the root target, `selector`, `listContainer`, and `listItemSelector` on the effect further refine which child elements are animated, following the same priority order as source resolution above.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly this could be problematic, no? If the Interaction has some key interaction-key and some of these selectors to select the correct source, but the effect also has a different key (effect-key), do these selectors apply to the target to? It might not be the intention..

Comment on lines +150 to +166
### Source element resolution (Interaction)

The source element is the element the trigger attaches to. Resolved in priority order:

1. **`listContainer` + `listItemSelector`** — trigger attaches to each element matching `listItemSelector` within the `listContainer`.
2. **`listContainer` only** — trigger attaches to each immediate child of the container.
3. **`listContainer` + `selector`** — trigger attaches to the element found via `querySelector` within each immediate child of the container.
4. **`selector` only** — trigger attaches to all elements matching `querySelectorAll` within the root `<interact-element>`.
5. **Fallback** — first child of `<interact-element>` (web) or the root element (react/vanilla).

### Target element resolution (Effect)

The target element is the element the effect animates. Resolved in priority order:

1. **`Effect.key`** — if provided, the target is the `<interact-element>` 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is really relevant only when using the list-features or selector-features, because when not using it the resolution is simple. Since this file intention is to first allow simple usage I think it will be better to not delve into it so early in the rules and instead reference a later section that handles "selectors and lists resolving".

```ts
import { generate } from '@wix/interact/web';
const css = generate(config);
// Include in <head>: <style>${css}</style>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why in comment?

Comment on lines +494 to +495
- Only valid for `viewEnter` + `type: 'once'` where source and target are the same element.
- Do NOT use for `hover`, `click`, or `viewEnter` with `repeat`/`alternate`/`state`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 2 are the same rule, but if you feel it needs to be emphasized then OK


---

## Common Pitfalls
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this section should be much higher IMO, as all of its points are critical. Each entry here will break animations if ignored, and as far it is down the file, the more likely that LLM will deem it less important

- **Stacking contexts and `viewProgress`**: Avoid `transform`, `filter`, `perspective`, `opacity < 1`, `will-change`, `contain: paint/layout/size` on the target or its ancestors. These can prevent or freeze ViewTimeline. Apply such styles to an inner child instead.
- **Perspective**: Prefer `transform: perspective(...)` inside keyframes. Use the CSS `perspective` property only when multiple children share the same `perspective-origin`.
- **Unknown preset options**: If you don't know the expected type/structure for a `namedEffect` param, omit it — rely on defaults rather than guessing.
- **Reduced motion**: Use conditions to provide gentler alternatives (shorter durations, fewer transforms, no perpetual motion) for users who prefer reduced motion.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mention forceReduceMotion?

Comment on lines +510 to +519
## Static API

| Method / Property | Description |
| :---------------------------------- | :------------------------------------------------------------------ |
| `Interact.create(config)` | Initialize with a config. Returns the instance. |
| `Interact.registerEffects(presets)` | Register named effect presets before `create`. |
| `Interact.destroy()` | Tear down all instances. |
| `Interact.forceReducedMotion` | `boolean` — force reduced-motion behavior. |
| `Interact.allowA11yTriggers` | `boolean` — enable accessibility triggers (`interest`, `activate`). |
| `Interact.setup(options)` | Configure global scroll/pointer/viewEnter options. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each entry here is either already explained and documented above (create, registerEffects), or the complete opposite it was never mentioned and its importance or when to use it is unclear at all (forceReducedMotion, setup, ...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants