@wix/interact
v2.4.0
Published
Declarative, configuration-driven interaction library — web-native, AI-ready, and framework-agnostic.
Readme
@wix/interact
Declarative, configuration-driven interaction library — web-native, AI-ready, and framework-agnostic.
Why Interact?
- Declarative — Define trigger-to-effect bindings in JSON; no imperative event wiring
- Web-native — Built on CSS, WAAPI, ViewTimeline, and DOM APIs; supports DOM management via Custom Elements
- Framework-agnostic — Web Components and vanilla JS integrations; React integration included
- AI-ready — JSON configs are machine-readable and provide guardrails; LLMs can generate and agents can validate them
- CSS generation —
generate(config)emits complete CSS for the whole config (@keyframes,view-timeline, transitions, FOUC rules) - Preset ecosystem — Plug in
@wix/motion-presetsfor 75+ ready-made effects. - Accessible — Built-in
activate(click + keyboard) andinterest(hover + focus) trigger variants
Install
npm install @wix/interactUse pre-made presets
npm install @wix/motion-presets@wix/motion-presets is optional but recommended — it provides the namedEffect library used in most examples below.
Quick Start
Using Web Components (recommended)
Web Components — wrap the target element with <interact-element>:
import { Interact, generate, type InteractConfig } from '@wix/interact/web';
import * as presets from '@wix/motion-presets'; // required when using namedEffect
Interact.registerEffects(presets); // required when using namedEffect
const config: InteractConfig = {
interactions: [
{
key: 'hero',
trigger: 'viewEnter',
effects: [{ effectId: 'hero-in' }],
},
],
effects: {
'hero-in': {
duration: 800,
easing: 'ease-out',
namedEffect: { type: 'FadeIn' }, // requires motion-presets
triggerType: 'once',
},
},
};
// render styles - e.g. for SSR
const interactCSS = generate(config, true);
// run on client - e.g. on pagereveal event
const instance = Interact.create(config);In <head> add:
<style>
${interactCSS}
/* Optional — keep the custom element from affecting layout */
interact-element {
display: contents;
}
</style>In the <body> add:
<interact-element data-interact-key="hero">
<section class="hero">Hello, animated world!</section>
</interact-element>Using React
A complete React example: register presets, generate CSS, mount the component, clean up on unmount.
import { useEffect } from 'react';
import type { InteractConfig } from '@wix/interact';
import { Interact, Interaction, generate } from '@wix/interact/react';
import * as presets from '@wix/motion-presets'; // optional
Interact.registerEffects(presets); // optional
const config: InteractConfig = {
//...
};
export function App () {
const interactCSS = generate(config, false);
// rest of App logic ...
useEffect(() => {
const instance = Interact.create(config);
return () => instance.destroy();
}, []);
return (
// can go to <head>
<style>{interactCSS}</style>
// ...
<Hero/>
// ...
)
}
// in components/Hero.jsx
export function Hero() {
return (
<Interaction tagName="section" interactKey="hero">
Hello, animated world!
</Interaction>
);
}Using Vanilla JS (you manage element lifecycle)
Vanilla JS — bind elements after they exist in the DOM:
import type { InteractConfig } from '@wix/interact';
import { Interact, add } from '@wix/interact';
const config: InteractConfig = {
//...
};
const instance = Interact.create(config);
add(document.querySelector('#hero'), 'hero');Entry Points
| Import | Use When |
| --------------------- | ----------------------------------------------------------- |
| @wix/interact | Vanilla JS — manual element binding via add()/remove(). |
| @wix/interact/web | Web Components — <interact-element> custom element. |
| @wix/interact/react | React — <Interaction> component with lifecycle. |
All three entry points export the same Interact class, generate() function, and types.
How It Works
Config ─┬─► Interact.create() ─► Trigger Observers ─► Effect Engine ─► Animation (via @wix/motion)
└─► generate() ────────► CSS (@keyframes, view-timeline, animations, transitions) ─► <head>generate(config) runs at build time or on the server to emit complete CSS for the entire config — maximizing offload of effect creation, binding, and running to the browser.
Interact also generates native view-timeline CSS declarations, so browsers that support it can drive scroll animations entirely without JS.
The InteractConfig shape:
type InteractConfig = {
interactions: Interaction[]; // trigger → effect bindings
effects?: Record<string, Effect>; // reusable effect definitions
sequences?: Record<string, SequenceConfig>; // staggered multi-effect timelines
conditions?: Record<string, Condition>; // media / selector gates
};Triggers
| Trigger | Fires On | Params |
| -------------- | -------------------------------------------- | --------------------------------------- |
| viewEnter | Element enters viewport | threshold?, inset? |
| viewProgress | While element scrolls through viewport | (use rangeStart/rangeEnd on effect) |
| hover | Pointer enters/leaves element | — |
| click | Element is clicked | — |
| activate | Click + keyboard (a11y variant of click) | — |
| interest | Hover + focus (a11y variant of hover) | — |
| pointerMove | While pointer moves over element or viewport | hitArea?, axis? |
| animationEnd | Another specified effect is finished | effectId |
Effects
| Effect Type | Use For |
| ------------------------------------- | -------------------------------------------------------------------------- |
| keyframeEffect | Inline keyframes — self-contained, no preset needed. |
| namedEffect | Registered presets from @wix/motion-presets (e.g. { type: 'FadeIn' }). |
| customEffect | Programmatic (element, progress) => void callback. |
| transition / transitionProperties | CSS state changes driven by stateAction (add/remove/toggle). |
Recipes
Each example is a complete InteractConfig — pass it to Interact.create(config).
Entrance animation
{
interactions: [{
key: 'hero',
trigger: 'viewEnter',
effects: [{ effectId: 'float-in' }],
}],
effects: {
'float-in': {
duration: 800,
easing: 'ease-out',
namedEffect: { type: 'FloatIn', direction: 'bottom' },
},
},
}Click effect
{
interactions: [{
key: 'cta',
trigger: 'click',
effects: [{ effectId: 'pulse' }],
}],
effects: {
'pulse': {
duration: 300,
keyframeEffect: {
name: 'pulse',
keyframes: [
{ transform: 'scale(1.08)', offset: 0.5 }
],
},
triggerType: 'repeat',
},
},
}Scroll-driven animations
{
interactions: [{
key: 'card',
trigger: 'viewProgress',
effects: [{ effectId: 'parallax' }],
}],
effects: {
'parallax': {
keyframeEffect: {
name: 'parallax',
keyframes: [
{ transform: 'translateY(-120px)' },
{ transform: 'translateY(120px)' },
],
},
rangeStart: { name: 'cover', offset: { value: 0, unit: 'percentage' } },
rangeEnd: { name: 'cover', offset: { value: 100, unit: 'percentage' } },
fill: 'both',
easing: 'linear',
},
},
}Hover toggle
CSS transition
{
interactions: [{
key: 'card',
trigger: 'hover',
effects: [{ effectId: 'lift', selector: '.card-figure' }],
}],
effects: {
'lift': {
transition: {
duration: 200,
easing: 'ease-out',
styleProperties: [
{ name: 'transform', value: 'scale(1.08)' },
{ name: 'box-shadow', value: '0 8px 16px rgb(0 0 0 / 0.15)' },
],
},
},
},
}CSS Animation
{
interactions: [{
key: 'card',
trigger: 'hover',
effects: [{ effectId: 'lift', selector: '.card-figure' }],
}],
effects: {
'lift': {
keyframeEffect: {
name: 'lift',
keyframes: [
{ transform: 'translateY(-80px)', boxShadow: '0 8px 16px rgb(0 0 0 / 0.15)' },
],
},
duration: 200,
easing: 'ease-out',
},
},
}Pointer-tracking
Keyframe effect
{
interactions: [{
key: 'card-wrapper',
trigger: 'pointerMove',
params: { hitArea: 'root', axis: 'x' },
effects: [{ effectId: 'follow-x', key: 'card' }],
}, {
key: 'card-wrapper',
trigger: 'pointerMove',
params: { hitArea: 'root', axis: 'y' },
effects: [{ effectId: 'follow-y', key: 'card' }],
}],
effects: {
'follow-x': {
keyframeEffect: {
name: 'follow-x',
keyframes: [
{ transform: 'rotateY(-45deg)' },
{ transform: 'rotateY(0px)' },
{ transform: 'rotateY(45deg)' },
],
},
easing: 'linear',
centeredToTarget: true,
},
'follow-y': {
keyframeEffect: {
name: 'follow-y',
keyframes: [
{ transform: 'rotateX(45deg)' },
{ transform: 'rotateX(0px)' },
{ transform: 'rotateX(-45deg)' },
],
},
easing: 'linear',
composite: 'add',
centeredToTarget: true,
},
},
}Custom effect
{
interactions: [{
key: 'spotlight',
trigger: 'pointerMove',
params: { hitArea: 'root' },
effects: [{ effectId: 'follow' }],
}],
effects: {
'follow': {
customEffect: (element: HTMLElement, progress: { x: number, y: number }) => {
element.style.setProperty('--x', `${progress.x * 100}%`);
element.style.setProperty('--y', `${progress.y * 100}%`);
},
},
},
}Common Pitfalls
overflow: hiddenbreaksviewProgress— Useoverflow: clipon all ancestors between the source and the scroll container.- Same element as source and target with
viewEnter— Must usetriggerType: 'once'. Other types cause re-entry loops. - Hit-area shift on
hover/pointerMove— Animating size/position of the hovered element shifts the hit area and causes jitter. Instead, animate a child viaselectoror a differentkey. registerEffects()must run beforeInteract.create()/generate()when usingnamedEffect.- FOUC prevention — requires injecting the output of
generate(config)into<head>. generate(config, useFirstChild)— Passtruefor<interact-element>(web),falsefor vanilla and React<Interaction>.<interact-element>must wrap exactly one child — the library targets:first-childby default.
AI & Agent Support
Interact's JSON-config surface is the differentiator: configs are serializable, schema-typed, and validate-able (guardrails) — no imperative DOM logic for an LLM to hallucinate.
AI agents can discover @wix/interact documentation through:
- llms.txt — structured docs index (llms.txt standard)
- llms-full.txt — all rules in a single file
Rules files ship with the package under rules/ — point your agent at them:
rules/full-lean.md— complete config spec, pitfalls, and constraintsrules/integration.md— integration entry points, lifecycle, style generationrules/viewenter.md— viewport entrance triggers (scroll-triggered animations)rules/viewprogress.md— scroll-driven animationsrules/click.md— click and activate triggersrules/hover.md— hover and interest triggersrules/pointermove.md— pointer-driven animations
Generation constraints for agents producing configs:
- Do not invent
namedEffecttypes — use only registered presets. - Do not attach DOM event listeners manually — use triggers.
- Do not use
overflow: hiddenon scroll-tracked ancestors — useoverflow: clip. - Always pre-render CSS with
generate(config)and inject into<head>. - Always call
Interact.registerEffects(presets)beforegenerate()andInteract.create()when usingnamedEffect.
Browser Support
- Modern browsers with the Web Animations API (Baseline).
adoptedStyleSheets(used bytransition/transitionProperties): Chrome 73+, Firefox 101+, Safari 16.4+, Edge 79+.- ViewTimeline: Chrome 115+; polyfilled via
fizbanelsewhere.
Related Packages
@wix/motion— low-level animation engine underneath Interact.@wix/motion-presets— ready-made effect catalog (entrance, scroll, hover, pointer).fizban— scroll-driven animation polyfill (bundled dependency).kuliso— pointer-driven animation polyfill (bundled dependency).
Documentation
- Getting Started
- API Reference —
Interactclass,InteractionController, standalone functions, types - Guides — triggers, effects, configuration, state, conditions, sequences
- Examples — entrance, click, hover, list patterns
- Web Components — integration via custom elements
- React Integration — React integration
